Skip to content

wuxianggujun/FeatherDoc

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

347 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FeatherDoc

Windows MSVC CI

Simplified Chinese | English

FeatherDoc is a modernized C++ library for reading and writing Microsoft Word .docx files.

Highlights

  • CMake 3.20+
  • C++20
  • MSVC-friendly build setup
  • Lightweight document editing APIs for paragraphs, runs, tables, images, lists, and style references
  • MSVC-safe XML parsing on open()
  • Streamed ZIP rewrite path on save()

Build

cmake -S . -B build
cmake --build build

Top-level builds enable BUILD_CLI by default, so the featherdoc_cli utility is built alongside the library unless you pass -DBUILD_CLI=OFF.

Build With MSVC

Open an x64 Visual Studio Developer Command Prompt first, or initialize the environment with VsDevCmd.bat -arch=x64 -host_arch=x64, then run:

cmake -S . -B build-msvc-nmake -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DBUILD_SAMPLES=ON -DBUILD_CLI=ON
cmake --build build-msvc-nmake
ctest --test-dir build-msvc-nmake --output-on-failure --timeout 60

Word Visual Smoke Check

On Windows hosts with Microsoft Word installed you can run a visual smoke check that:

  • builds a dedicated sample document covering table layout features
  • exports the generated .docx through Word's rendering engine as PDF
  • renders each PDF page to PNG for manual or AI-assisted review
powershell -ExecutionPolicy Bypass -File .\scripts\run_word_visual_smoke.ps1

For the fixed-grid merge/unmerge sample quartet specifically, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_fixed_grid_merge_unmerge_regression.ps1

For a before/after Word-rendered check of the new template-table CLI row/cell commands, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_template_table_cli_visual_regression.ps1

That regression now generates a dedicated baseline document covering body, section-header, and section-footer template tables, applies set-template-table-cell-text, append-template-table-row, insert-template-table-row-after, insert-template-table-row-before, and remove-template-table-row to each target table in turn, then captures Word-rendered baseline/mutated evidence plus per-case and aggregate before/after contact sheets under output/template-table-cli-visual-regression/.

For a dedicated before/after Word-rendered check of the bookmark-targeted table workflow, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_template_table_cli_bookmark_visual_regression.ps1

That regression generates a body-only baseline document with one retained table plus one target table addressed by both a bookmark paragraph immediately before it and a bookmark inside its first data cell. It applies set-template-table-row-texts and set-template-table-cell-block-texts through --bookmark, then writes Word-rendered baseline/mutated evidence plus per-case and aggregate before/after contact sheets under output/template-table-cli-bookmark-visual-regression/.

For a dedicated Word-rendered overlap check of floating-image anchor z-order, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_floating_image_z_order_visual_regression.ps1

That regression builds the dedicated sample_floating_image_z_order_visual fixture, verifies both anchored images through CLI inspection and extraction, then captures the rendered first page so you can confirm the orange floating image overlaps and appears above the blue one under output/floating-image-z-order-visual-regression/.

For a before/after Word-rendered check of section-scoped template-table row/cell commands across --kind default/even/first, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_template_table_cli_section_kind_visual_regression.ps1

That regression generates one shared baseline document covering section-header --kind default, section-header --kind even, section-footer --kind first, and section-footer --kind default, applies the same row/cell mutation chain to each target table, then captures the relevant Word-rendered page-01 / page-02 / page-03 evidence instead of only the first page so the kind-specific rendering path is visible under output/template-table-cli-section-kind-visual-regression/.

For a dedicated before/after Word-rendered check of template-table row commands across section-scoped --kind default/even/first, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_template_table_cli_section_kind_row_visual_regression.ps1

That regression generates four dedicated baseline documents covering section-header --kind default insert-row-before, section-header --kind even append-row, section-footer --kind first remove-row, and section-footer --kind default insert-row-after, applies the matching row mutation plus set-template-table-cell-text where needed, then captures the relevant Word-rendered page-01 / page-02 / page-03 evidence under output/template-table-cli-section-kind-row-visual-regression/.

For a before/after Word-rendered check of template-table column commands across section-scoped --kind default/even/first, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_template_table_cli_section_kind_column_visual_regression.ps1

That regression generates four dedicated baseline documents covering section-header --kind default insert-before, section-header --kind even remove-column, section-footer --kind first insert-after, and section-footer --kind default remove-column, applies the matching column mutation chain plus set-template-table-cell-text where needed, then captures the relevant Word-rendered page-01 / page-02 / page-03 evidence under output/template-table-cli-section-kind-column-visual-regression/.

For a before/after Word-rendered check of template-table merge/unmerge commands across section-scoped --kind default/even/first, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_template_table_cli_section_kind_merge_unmerge_visual_regression.ps1

That regression generates four dedicated baseline documents covering section-header --kind default merge-right, section-header --kind even unmerge-right, section-footer --kind first merge-down, and section-footer --kind default unmerge-down, applies the matching merge/unmerge CLI mutation, verifies the underlying XML merge markers, then captures the relevant Word-rendered page-01 / page-02 / page-03 evidence under output/template-table-cli-section-kind-merge-unmerge-visual-regression/.

For a before/after Word-rendered check of the new template-table CLI merge/unmerge commands, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_template_table_cli_merge_unmerge_visual_regression.ps1

That regression generates four dedicated baseline documents covering section-header horizontal merge, section-footer vertical merge, and body horizontal/vertical unmerge scenarios, applies merge-template-table-cells or unmerge-template-table-cells, then captures Word-rendered baseline/mutated evidence plus per-case and aggregate before/after contact sheets under output/template-table-cli-merge-unmerge-visual-regression/.

For a before/after Word-rendered check of the template-table CLI row/cell commands on direct header / footer parts, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_template_table_cli_direct_visual_regression.ps1

That regression generates one shared baseline document covering direct header, direct footer, and body target tables, applies set-template-table-cell-text, append-template-table-row, insert-template-table-row-after, insert-template-table-row-before, and remove-template-table-row through the direct-part selector model, then captures Word-rendered baseline/mutated evidence plus per-case and aggregate before/after contact sheets under output/template-table-cli-direct-visual-regression/.

For a before/after Word-rendered check of the new template-table CLI column commands, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_template_table_cli_column_visual_regression.ps1

That regression generates three dedicated baseline documents covering section-header insert-before, section-footer insert-after, and body remove-column scenarios, applies insert-template-table-column-before, insert-template-table-column-after, remove-template-table-column, and set-template-table-cell-text where needed, then captures Word-rendered baseline/mutated evidence plus per-case and aggregate before/after contact sheets under output/template-table-cli-column-visual-regression/.

For a before/after Word-rendered check of the same template-table column commands on direct header / footer parts, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_template_table_cli_direct_column_visual_regression.ps1

That regression generates three dedicated baseline documents covering direct header insert-before, direct footer insert-after, and body remove-column scenarios, applies the same CLI column mutations, then captures Word-rendered baseline/mutated evidence plus per-case and aggregate before/after contact sheets under output/template-table-cli-direct-column-visual-regression/.

For a before/after Word-rendered check of template-table merge/unmerge commands on direct header / footer parts, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_template_table_cli_direct_merge_unmerge_visual_regression.ps1

That regression generates three dedicated baseline documents covering direct header merge-right, direct footer unmerge-down, and body merge-down scenarios, applies merge-template-table-cells or unmerge-template-table-cells, then captures Word-rendered baseline/mutated evidence plus per-case and aggregate before/after contact sheets under output/template-table-cli-direct-merge-unmerge-visual-regression/.

If you want the quartet regression to immediately emit an AI review task package after the Word screenshot bundle is ready, run:

powershell -ExecutionPolicy Bypass -File .\scripts\run_fixed_grid_merge_unmerge_regression.ps1 `
    -PrepareReviewTask `
    -ReviewMode review-only

That script builds the four dedicated sample targets, generates one .docx per case under output/fixed-grid-merge-unmerge-regression/, and, unless you pass -SkipVisual, also runs the existing Word PDF/PNG capture flow under each case's visual/ directory. It also writes a root-level aggregate contact sheet plus review_manifest.json, review_checklist.md, and final_review.md so human or AI reviewers can inspect the whole fixed-grid quartet from one place. When -PrepareReviewTask is enabled, it also invokes prepare_word_review_task.ps1 automatically and records the generated task metadata back into the root summary.json and review_manifest.json.

Artifacts are written under output/word-visual-smoke/, including the source .docx and exported .pdf, plus an evidence/ directory for per-page .png renders and contact sheets, a report/ directory for the generated checklist and summary data, generated review_result.json and final_review.md skeletons, and a reserved repair/ directory for iterative fix candidates. The script now also validates two things before you trust the result:

  • generated smoke samples must contain the expected core DOCX ZIP entries
  • exported PDF page count must match both summary.json and the rendered PNG count

Pass -InputDocx <path> when you want to run the same render-and-capture flow against an existing document instead of the bundled smoke sample. The recommended review-and-repair workflow is documented in docs/automation/word_visual_workflow_zh.rst.

Use -SkipBuild only when the executable under -BuildDir is known to be current. If the smoke sample generator is stale, rerun without -SkipBuild or rebuild that directory first.

When you want to hand the task to an AI agent consistently, generate a review task package first:

powershell -ExecutionPolicy Bypass -File .\scripts\prepare_word_review_task.ps1 `
    -DocxPath .\path\to\target.docx `
    -Mode review-only

For the fixed-grid regression bundle generated by run_fixed_grid_merge_unmerge_regression.ps1, use:

powershell -ExecutionPolicy Bypass -File .\scripts\prepare_word_review_task.ps1 `
    -FixedGridRegressionRoot .\output\fixed-grid-merge-unmerge-regression `
    -Mode review-only

Use -Mode review-and-repair when the agent is allowed to fix generation logic and rerun the visual review loop. The script generates a task directory with task_prompt.md, task_manifest.json, and dedicated evidence/, report/, and repair/ directories, so the AI no longer has to guess paths or output structure. The intended handoff flow is:

  1. Run prepare_word_review_task.ps1.
  2. Open the generated task_prompt.md.
  3. Send the full prompt to the AI agent and require it to first run scripts/run_word_visual_smoke.ps1 -InputDocx ... -OutputDir <task dir>.
  4. Require the agent to review the generated PDF/PNG evidence and write its conclusion back into report/review_result.json and report/final_review.md.
  5. If the mode is review-and-repair, allow the agent to iterate under repair/fix-XX/ and rerun the full visual review loop after each fix.

Every task generation also refreshes stable pointer files under output/word-visual-smoke/tasks/, including latest_task.json plus a source-kind-specific file such as latest_fixed-grid-regression-bundle_task.json or latest_template-table-cli-selector-visual-regression-bundle_task.json, so external automation can resolve the newest task package without guessing timestamped directory names.

When you want to consume those pointers directly, use:

powershell -ExecutionPolicy Bypass -File .\scripts\open_latest_word_review_task.ps1

Or for the newest fixed-grid bundle task specifically:

powershell -ExecutionPolicy Bypass -File .\scripts\open_latest_word_review_task.ps1 `
    -SourceKind fixed-grid-regression-bundle `
    -PrintPrompt

Or for a curated visual regression bundle emitted by the release gate:

powershell -ExecutionPolicy Bypass -File .\scripts\open_latest_word_review_task.ps1 `
    -SourceKind template-table-cli-selector-visual-regression-bundle `
    -PrintPrompt

For automation-friendly raw outputs, use:

powershell -ExecutionPolicy Bypass -File .\scripts\open_latest_word_review_task.ps1 `
    -SourceKind fixed-grid-regression-bundle `
    -PrintField task_dir

powershell -ExecutionPolicy Bypass -File .\scripts\open_latest_word_review_task.ps1 `
    -SourceKind fixed-grid-regression-bundle `
    -PromptOnly

For the fixed-grid path specifically, the shorter wrappers are:

powershell -ExecutionPolicy Bypass -File .\scripts\open_latest_fixed_grid_review_task.ps1 `
    -PrintField task_dir

powershell -ExecutionPolicy Bypass -File .\scripts\print_latest_fixed_grid_review_prompt.ps1

For fixed-grid bundle tasks, the task package also copies the aggregate contact sheet, bundle summary, bundle review manifest, aggregate first-page PNGs, plus the source bundle's checklist/final-review markdown into the task directory, so the reviewer can start from a single visual bundle instead of discovering each case path manually.

Rendered Examples

The screenshots below come from actual Word-rendered validation artifacts saved in the repository, so README readers can see the current layout coverage, rendering quality, and screenshot-backed review surface directly. The focused row below intentionally mixes two standalone fixed-grid closure samples, the aggregate quartet signoff bundle, and one Chinese business-template result so the library's editing surface is visible at a glance.

Full Word visual smoke contact sheet

Top: the full 6-page Word visual smoke contact sheet generated from the current validation flow, covering tables, pagination, merges, text direction, fixed-grid width editing, and mixed RTL/LTR/CJK review content.

Word-rendered standalone merge_right fixed-grid sample page Word-rendered standalone merge_down fixed-grid sample page Fixed-grid merge and unmerge regression contact sheet Word-rendered Chinese invoice template sample page

Bottom row, left to right: a standalone merge_right() closure check after reopen, a standalone merge_down() closure check after reopen, the aggregate fixed-grid quartet signoff bundle, and a Chinese invoice template rendered through Microsoft Word. The first two screenshots come from sample_merge_right_fixed_grid.cpp and sample_merge_down_fixed_grid.cpp; they make the fixed-width closure signal obvious without needing to read XML. The fourth image comes from sample_chinese_template.cpp and shows CJK text plus business-table output in Word rather than synthetic mockups.

To regenerate the exact focused gallery pages, build and run the dedicated sample targets first. Adjust the executable path if your generator places the sample binaries somewhere other than the build root:

cmake --build build-msvc-nmake --target `
    featherdoc_sample_merge_right_fixed_grid `
    featherdoc_sample_merge_down_fixed_grid `
    featherdoc_sample_chinese_template

.\build-msvc-nmake\featherdoc_sample_merge_right_fixed_grid.exe `
    .\output\sample-merge-right-fixed-grid\merge_right_fixed_grid.docx

.\build-msvc-nmake\featherdoc_sample_merge_down_fixed_grid.exe `
    .\output\sample-merge-down-fixed-grid\merge_down_fixed_grid.docx

.\build-msvc-nmake\featherdoc_sample_chinese_template.exe `
    .\samples\chinese_invoice_template.docx `
    .\output\sample-chinese-template\sample_chinese_invoice_output.docx

powershell -ExecutionPolicy Bypass -File .\scripts\run_word_visual_smoke.ps1 `
    -InputDocx .\output\sample-merge-right-fixed-grid\merge_right_fixed_grid.docx `
    -OutputDir .\output\word-visual-sample-merge-right-fixed-grid

powershell -ExecutionPolicy Bypass -File .\scripts\run_word_visual_smoke.ps1 `
    -InputDocx .\output\sample-merge-down-fixed-grid\merge_down_fixed_grid.docx `
    -OutputDir .\output\word-visual-sample-merge-down-fixed-grid

powershell -ExecutionPolicy Bypass -File .\scripts\run_word_visual_smoke.ps1 `
    -InputDocx .\output\sample-chinese-template\sample_chinese_invoice_output.docx `
    -OutputDir .\output\word-visual-sample-chinese-template

To rerun the fixed-grid quartet that feeds the aggregate contact sheet and prepare a ready-to-review task package:

powershell -ExecutionPolicy Bypass -File .\scripts\run_fixed_grid_merge_unmerge_regression.ps1 `
    -PrepareReviewTask `
    -ReviewMode review-only

To refresh the repository-side gallery PNGs after those sample renders or a new release-gate pass, use:

powershell -ExecutionPolicy Bypass -File .\scripts\refresh_readme_visual_assets.ps1

To jump from these screenshots to a review-task handoff, start with VISUAL_VALIDATION_QUICKSTART.md and then use VISUAL_VALIDATION.md or VISUAL_VALIDATION.zh-CN.md for the longer mapping.

For a one-shot local gate that also refreshes the repository gallery, use:

powershell -ExecutionPolicy Bypass -File .\scripts\run_word_visual_release_gate.ps1 `
    -RefreshReadmeAssets

After the screenshot-backed review tasks you care about are signed off, including any curated visual regression bundles emitted by the gate, sync the final verdict back into the gate summary with:

powershell -ExecutionPolicy Bypass -File .\scripts\sync_latest_visual_review_verdict.ps1

That shortest sync path now walks every latest_*_task.json pointer it can resolve under the task root, so later curated visual-regression bundle tasks from run_word_visual_release_gate.ps1 are promoted together with the classic document / fixed-grid / section-page-setup / page-number-fields tasks.

If you need to override the inferred gate/release paths manually, use:

powershell -ExecutionPolicy Bypass -File .\scripts\sync_visual_review_verdict.ps1 `
    -GateSummaryJson .\output\word-visual-release-gate\report\gate_summary.json

CLI

featherdoc_cli is a small command-line wrapper around the current inspection and editing APIs for sections, styles, numbering, page setup, bookmarks, images, and template parts.

featherdoc_cli inspect-sections input.docx
featherdoc_cli inspect-sections input.docx --json
featherdoc_cli inspect-styles input.docx --style Strong --json
featherdoc_cli inspect-runs input.docx 1 --run 0 --json
featherdoc_cli inspect-template-runs input.docx 1 --run 0 --json
featherdoc_cli inspect-numbering input.docx --definition 1 --json
featherdoc_cli inspect-page-setup input.docx --section 1 --json
featherdoc_cli inspect-bookmarks input.docx --part header --index 0 --bookmark header_rows --json
featherdoc_cli inspect-images input.docx --relationship-id rId5 --json
featherdoc_cli ensure-table-style input.docx ReportTable --name "Report Table" --based-on TableGrid --output styled.docx --json
featherdoc_cli inspect-header-parts input.docx --json
featherdoc_cli inspect-footer-parts input.docx
featherdoc_cli insert-section input.docx 1 --no-inherit --output inserted.docx --json
featherdoc_cli copy-section-layout input.docx 0 2 --output copied.docx
featherdoc_cli move-section input.docx 2 0 --output reordered.docx
featherdoc_cli remove-section input.docx 3 --output trimmed.docx
featherdoc_cli assign-section-header input.docx 2 0 --kind even --output shared-header.docx --json
featherdoc_cli assign-section-footer input.docx 2 1 --output shared-footer.docx --json
featherdoc_cli remove-section-header input.docx 2 --kind even --output detached-header.docx
featherdoc_cli remove-section-footer input.docx 1 --kind first --output detached-footer.docx
featherdoc_cli remove-header-part input.docx 1 --output headers-pruned.docx
featherdoc_cli move-header-part input.docx 1 0 --output headers-reordered.docx --json
featherdoc_cli remove-footer-part input.docx 0 --output footers-pruned.docx --json
featherdoc_cli move-footer-part input.docx 1 0 --output footers-reordered.docx --json
featherdoc_cli show-section-header input.docx 1 --kind even
featherdoc_cli show-section-footer input.docx 2 --json
featherdoc_cli set-section-footer input.docx 0 --text "Page 1" --output footer.docx --json
featherdoc_cli set-section-header input.docx 2 --kind even --text-file header.txt --json
featherdoc_cli append-page-number-field input.docx --part section-header --section 1 --output page-number.docx --json
featherdoc_cli set-template-table-from-json report.docx --bookmark line_items_table --patch-file row_patch.json --output report-updated.docx --json
featherdoc_cli set-template-tables-from-json report.docx --patch-file multi_table_patch.json --output report-updated.docx --json
featherdoc_cli validate-template input.docx --part body --slot customer:text --slot line_items:table_rows --json
featherdoc_cli validate-template-schema input.docx --target section-header --section 0 --slot header_title:text --target section-footer --section 0 --slot footer_company:text --slot footer_summary:block --json
featherdoc_cli validate-template-schema input.docx --schema-file template-schema.json --json
featherdoc_cli export-template-schema input.docx --output template-schema.json --json
featherdoc_cli export-template-schema input.docx --section-targets --output section-template-schema.json --json
featherdoc_cli export-template-schema input.docx --resolved-section-targets --output resolved-section-template-schema.json --json
featherdoc_cli normalize-template-schema template-schema.json --output normalized-template-schema.json --json
featherdoc_cli lint-template-schema template-schema.json --json
featherdoc_cli repair-template-schema template-schema.json --output repaired-template-schema.json --json
featherdoc_cli merge-template-schema shared-template-schema.json invoice-template-schema.json --output merged-template-schema.json --json
featherdoc_cli patch-template-schema committed-template-schema.json --patch-file schema.patch.json --output patched-template-schema.json --json
featherdoc_cli build-template-schema-patch committed-schema.json generated-schema.json --output schema.patch.json --json
featherdoc_cli diff-template-schema old-template-schema.json new-template-schema.json --json
featherdoc_cli diff-template-schema committed-schema.json generated-schema.json --fail-on-diff --json
featherdoc_cli check-template-schema input.docx --schema-file committed-schema.json --resolved-section-targets --output generated-schema.json --json
pwsh -ExecutionPolicy Bypass -File .\scripts\freeze_template_schema_baseline.ps1 -InputDocx .\template.docx -SchemaOutput .\template.schema.json -ResolvedSectionTargets
pwsh -ExecutionPolicy Bypass -File .\scripts\check_template_schema_baseline.ps1 -InputDocx .\template.docx -SchemaFile .\template.schema.json -ResolvedSectionTargets -GeneratedSchemaOutput .\generated-template.schema.json -RepairedSchemaOutput .\repaired-template.schema.json
pwsh -ExecutionPolicy Bypass -File .\scripts\check_template_schema_manifest.ps1 -ManifestPath .\baselines\template-schema\manifest.json -BuildDir build-codex-clang-compat -RepairedSchemaOutputDir .\output\template-schema-manifest-repairs
pwsh -ExecutionPolicy Bypass -File .\scripts\register_template_schema_manifest_entry.ps1 -Name template-name -InputDocx .\template.docx
pwsh -ExecutionPolicy Bypass -File .\scripts\new_project_template_smoke_onboarding_plan.ps1 -ManifestPath .\samples\project_template_smoke.manifest.json -BuildDir build-codex-clang-compat
pwsh -ExecutionPolicy Bypass -File .\scripts\discover_project_template_smoke_candidates.ps1 -ManifestPath .\samples\project_template_smoke.manifest.json
pwsh -ExecutionPolicy Bypass -File .\scripts\discover_project_template_smoke_candidates.ps1 -ManifestPath .\samples\project_template_smoke.manifest.json -Json -IncludeRegistered -IncludeExcluded -OutputPath .\output\project-template-smoke\candidate_discovery.json -FailOnUnregistered
pwsh -ExecutionPolicy Bypass -File .\scripts\check_project_template_smoke_manifest.ps1 -ManifestPath .\samples\project_template_smoke.manifest.json -BuildDir build-codex-clang-compat -CheckPaths
pwsh -ExecutionPolicy Bypass -File .\scripts\describe_project_template_smoke_manifest.ps1 -ManifestPath .\samples\project_template_smoke.manifest.json -SummaryJson .\output\project-template-smoke\summary.json -BuildDir build-codex-clang-compat
pwsh -ExecutionPolicy Bypass -File .\scripts\register_project_template_smoke_manifest_entry.ps1 -Name contract-template -ManifestPath .\samples\project_template_smoke.manifest.json -InputDocx .\samples\chinese_invoice_template.docx -SchemaValidationFile .\baselines\template-schema\chinese_invoice_template.schema.json -SchemaBaselineFile .\baselines\template-schema\chinese_invoice_template.schema.json -VisualSmokeOutputDir .\output\project-template-smoke\contract-template-visual -ReplaceExisting
pwsh -ExecutionPolicy Bypass -File .\scripts\run_project_template_smoke.ps1 -ManifestPath .\samples\project_template_smoke.manifest.json -BuildDir build-codex-clang-compat -OutputDir output/project-template-smoke
pwsh -ExecutionPolicy Bypass -File .\scripts\sync_project_template_smoke_visual_verdict.ps1 -SummaryJson .\output\project-template-smoke\summary.json
pwsh -ExecutionPolicy Bypass -File .\scripts\render_template_document.ps1 -InputDocx .\samples\chinese_invoice_template.docx -PlanPath .\samples\chinese_invoice_template.render_plan.json -OutputDocx .\output\rendered\invoice.docx -SummaryJson .\output\rendered\invoice.render.summary.json -BuildDir build-codex-clang-compat -SkipBuild

For project-level smoke checks across several real templates, use scripts/run_project_template_smoke.ps1. Each manifest entry can point at a committed .docx directly or prepare one first via prepare_sample_target / prepare_argument, then opt into any combination of template_validations, schema_validation, schema_baseline, and optional visual_smoke. The wrapper writes per-entry artifacts plus aggregate summary.json and summary.md so you can review a whole template pack in one place. See samples/project_template_smoke.manifest.json for a runnable repository example. schema_baseline results now also record whether the committed schema is lint-clean, how many lint issues were found, and any repaired candidate path emitted by the baseline gate.

To validate the manifest contract before running the full harness, use scripts/check_project_template_smoke_manifest.ps1. The sample manifest now includes a local $schema reference to samples/project_template_smoke.manifest.schema.json, so JSON-aware editors can surface field completions and shape errors earlier. The runtime harness also performs the same upfront validation and stops immediately on malformed entries.

To maintain the manifest itself, use scripts/describe_project_template_smoke_manifest.ps1 for a readable overview of registered entries plus their latest smoke status, and scripts/register_project_template_smoke_manifest_entry.ps1 to add or update one entry without hand-editing JSON. register_* accepts direct -SchemaValidationFile / -SchemaBaselineFile flags for common cases and can also load complex template_validations or schema_validation.targets arrays from JSON files via -TemplateValidationsFile and -SchemaValidationTargetsFile. Before adding real templates, run scripts/new_project_template_smoke_onboarding_plan.ps1 for a non-mutating onboarding plan that combines candidate discovery with per-template freeze_template_schema_baseline.ps1 and register_project_template_smoke_manifest_entry.ps1 commands. The plan writes plan.json, plan.md, and candidate_discovery.json under output/ so you can review schema baseline paths, visual smoke output directories, and final strict-preflight commands before touching the manifest. You can also run scripts/discover_project_template_smoke_candidates.ps1 to list tracked .docx / .dotx candidates that are not yet registered and print ready-to-run register_project_template_smoke_manifest_entry.ps1 commands with unique suggested entry names. Add -FailOnUnregistered when you want the same scan to act as a coverage gate and return a non-zero exit code while any tracked template candidate is still missing from the manifest; intentional non-template fixtures can be listed under candidate_exclusions. The sample manifest now includes the committed samples/chinese_invoice_template.docx as a real-template entry with schema validation, schema-baseline checking, and Word visual smoke. When a reviewer later updates any referenced review_result.json, rerun scripts/sync_project_template_smoke_visual_verdict.ps1 to refresh both summary.json and summary.md with the latest entry-level review_status/review_verdict, top-level visual_verdict, and pending or undetermined visual-review counts. The describe helper now surfaces those latest visual verdict fields as part of the maintenance view. When the same smoke summary is already wired into run_release_candidate_checks.ps1, pass -ReleaseCandidateSummaryJson <report/summary.json> -RefreshReleaseBundle to the sync script so it also rewrites the release-candidate summary.json, final_review.md, START_HERE.md, ARTIFACT_GUIDE.md, REVIEWER_CHECKLIST.md, and release_handoff.md without rerunning the full preflight.

When you want a single-template render entrypoint instead of a repository-level smoke manifest, use scripts/render_template_document.ps1. It reads one JSON plan and chains fill-bookmarks, replace-bookmark-paragraphs, replace-bookmark-table-rows, and apply-bookmark-block-visibility into one repeatable .docx render flow. The script treats missing bookmarks as a hard failure so template drift is caught early, and it can emit a machine-readable summary JSON for CI or review tooling. See samples/template_render_plan.schema.json and samples/chinese_invoice_template.render_plan.json for a runnable example.

If you do not want to hand-author the first render plan, start with scripts/export_template_render_plan.ps1. It inspects body, header-part, and footer-part bookmarks, classifies text, block, table_rows, and block_range bookmarks into the four render-plan arrays, and writes a draft JSON that can be edited and then fed back into render_template_document.ps1. The draft uses TODO: <bookmark_name> placeholders for text and paragraph slots, keeps table-row drafts empty, and defaults block-visibility entries to visible: true.

If you want to keep business data separate from the exported draft, follow with scripts/patch_template_render_plan.ps1. It reads one base render plan plus a patch document, matches patch entries by bookmark_name plus optional part/index/section/kind selectors, and overwrites only the payload fields in the base plan. When a bookmark name is unique, the patch can stay minimal and only provide bookmark_name plus text, paragraphs, rows, or visible. Pass -RequireComplete when you want the script to fail on leftover TODO placeholders or empty table-row drafts before rendering. See samples/chinese_invoice_template.render_patch.json for a runnable example.

If you want one command that starts from a template plus a patch JSON and ends with a final .docx, use scripts/render_template_document_from_patch.ps1. It runs export, patch, and render in order, resolves featherdoc_cli once for the whole pipeline, enables -RequireComplete on the patch step by default, and can optionally keep the exported draft plus the patched render plan for review.

pwsh -ExecutionPolicy Bypass -File .\scripts\render_template_document_from_patch.ps1 -InputDocx .\samples\chinese_invoice_template.docx -PatchPlanPath .\samples\chinese_invoice_template.render_patch.json -OutputDocx .\output\rendered\invoice.from-patch.docx -SummaryJson .\output\rendered\invoice.from-patch.summary.json -DraftPlanOutput .\output\rendered\invoice.draft.render-plan.json -PatchedPlanOutput .\output\rendered\invoice.filled.render-plan.json -BuildDir build-codex-clang-compat -SkipBuild

If you want to keep the edit surface at the business-data level instead of hand-editing patch JSON, use scripts/convert_render_data_to_patch_plan.ps1 together with samples/template_render_data_mapping.schema.json. The mapping file binds one JSON source path to each bookmark_text, bookmark_paragraphs, bookmark_table_rows, or bookmark_block_visibility entry, so the generated patch stays reproducible while business data remains schema-shaped.

If you already have a render-plan draft and want a starting mapping file instead of writing one from scratch, run scripts/export_render_data_mapping_draft.ps1. It copies bookmark targets plus optional part/index/section/kind selectors and fills each source with an editable path derived from the bookmark name.

pwsh -ExecutionPolicy Bypass -File .\scripts\export_render_data_mapping_draft.ps1 -InputPlan .\samples\chinese_invoice_template.render_plan.json -OutputMapping .\output\rendered\invoice.render_data_mapping.draft.json -SummaryJson .\output\rendered\invoice.render_data_mapping.draft.summary.json -SourceRoot data

If you want a ready-to-edit data workspace directly from a .docx template, run scripts/prepare_template_render_data_workspace.ps1. It chains export_template_render_plan, export_render_data_mapping_draft, export_render_data_skeleton, and lint_render_data_mapping into one entry point, producing a render plan, mapping draft, editable data skeleton, and a workspace summary. This is the most direct setup when users should edit JSON business data first and render the final document later. The prepared workspace now also includes a START_HERE.zh-CN.md guide that points users to the JSON file they should edit first, the validation command that confirms the edited data is complete, and the render command they should run next.

pwsh -ExecutionPolicy Bypass -File .\scripts\prepare_template_render_data_workspace.ps1 -InputDocx .\samples\chinese_invoice_template.docx -WorkspaceDir .\output\rendered\invoice.workspace -SummaryJson .\output\rendered\invoice.workspace\summary.json -BuildDir build-codex-clang-compat -SkipBuild

After the workspace is prepared, use scripts/render_template_document_from_workspace.ps1 as the matching wrapper. It resolves the template, mapping, and data JSON from the workspace summary or workspace directory, so the user-facing flow becomes:

  1. edit the data JSON,
  2. run validate_render_data_mapping.ps1 -WorkspaceDir ... -RequireComplete and confirm status=completed plus remaining_placeholder_count=0,
  3. render the final .docx from the workspace.

If the data file still contains scaffold placeholders such as TODO:, the wrapper reports that directly instead of surfacing a lower-level patch/render error first. Pass -ReportMarkdown to also write a human-readable report, so users can open the .md file for the verdict and remaining slots before reading the JSON summary. Remaining-slot entries also use the mapping to point back to the data JSON fields users should inspect.

pwsh -ExecutionPolicy Bypass -File .\scripts\validate_render_data_mapping.ps1 -WorkspaceDir .\output\rendered\invoice.workspace -SummaryJson .\output\rendered\invoice.workspace\validation.summary.json -ReportMarkdown .\output\rendered\invoice.workspace\validation.report.md -BuildDir build-codex-clang-compat -SkipBuild -RequireComplete
pwsh -ExecutionPolicy Bypass -File .\scripts\render_template_document_from_workspace.ps1 -WorkspaceDir .\output\rendered\invoice.workspace -OutputDocx .\output\rendered\invoice.final.docx -SummaryJson .\output\rendered\invoice.workspace\render.summary.json

For direct edits to an existing .docx, use scripts/edit_document_from_plan.ps1. The JSON file is an edit instruction plan, not a JSON copy of the whole Word document: the script applies the operations to the original .docx in order, then writes a new .docx plus a summary. This is the matching workflow for “existing document + edit instructions = new document”.

{
  "operations": [
    {
      "op": "replace_bookmark_text",
      "bookmark": "customer_name",
      "text": "FeatherDoc Co."
    },
    {
      "op": "replace_bookmark_paragraphs",
      "bookmark": "note_lines",
      "paragraphs": ["First paragraph", "Second paragraph"]
    },
    {
      "op": "replace_bookmark_table_rows",
      "bookmark": "line_item_row",
      "rows": [["Service", "Description", "Amount"]]
    },
    {
      "op": "set_table_cell_text",
      "table_index": 0,
      "row_index": 3,
      "cell_index": 2,
      "text": "4,000.00"
    },
    {
      "op": "set_table_cell_fill",
      "table_index": 0,
      "row_index": 3,
      "cell_index": 2,
      "background_color": "FFF2CC"
    },
    {
      "op": "set_table_cell_border",
      "table_index": 0,
      "row_index": 3,
      "cell_index": 2,
      "edge": "right",
      "style": "single",
      "thickness": 16,
      "color": "FF0000"
    },
    {
      "op": "set_table_row_height",
      "table_index": 0,
      "row_index": 3,
      "height_twips": 720,
      "rule": "exact"
    },
    {
      "op": "set_table_cell_vertical_alignment",
      "table_index": 0,
      "row_index": 3,
      "cell_index": 2,
      "alignment": "center"
    },
    {
      "op": "set_table_cell_horizontal_alignment",
      "table_index": 0,
      "row_index": 3,
      "cell_index": 2,
      "alignment": "right"
    },
    {
      "op": "replace_text",
      "find": "old term",
      "replace": "new term"
    },
    {
      "op": "set_text_style",
      "text_contains": "new term",
      "bold": true,
      "font_family": "Segoe UI",
      "east_asia_font_family": "Microsoft YaHei",
      "language": "en-US",
      "east_asia_language": "zh-CN",
      "color": "C00000",
      "font_size_points": 14
    },
    {
      "op": "delete_paragraph_contains",
      "text_contains": "optional note"
    },
    {
      "op": "set_paragraph_horizontal_alignment",
      "text_contains": "First paragraph",
      "alignment": "center"
    },
    {
      "op": "set_paragraph_spacing",
      "text_contains": "First paragraph",
      "before_twips": 120,
      "after_twips": 240,
      "line_twips": 360,
      "line_rule": "exact"
    },
    {
      "op": "set_table_column_width",
      "table_index": 0,
      "column_index": 1,
      "width_twips": 5200
    },
    {
      "op": "merge_table_cells",
      "table_index": 0,
      "row_index": 3,
      "cell_index": 0,
      "direction": "right",
      "count": 2
    }
  ]
}

Table-cell vertical alignment supports top / center / bottom / both; table-cell and normal body-paragraph horizontal alignment both support left / center / right / both. Table layout edits include column width and cell merge/unmerge in the right or down direction. Cell appearance edits include background fill, single-edge border style, border thickness, and border color. Text style edits include bold, text color, font size, Latin font, CJK font, primary language, and East Asian language metadata. Paragraph layout edits include spacing before, spacing after, and line spacing. Body paragraphs can be targeted by paragraph_index or by text_contains. If a document has no bookmarks, use replace_text for ordinary body text replacement; it searches the merged visible paragraph text, so it handles common Word run splitting. For vertical centering to be visible in Word, pair it with set_table_row_height so the row has an explicit height. Use merge_table_cells to merge table cells and unmerge_table_cells to split an existing merge; count controls how many cells the merge spans.

pwsh -ExecutionPolicy Bypass -File .\scripts\edit_document_from_plan.ps1 -InputDocx .\samples\chinese_invoice_template.docx -EditPlan .\output\rendered\invoice.edit_plan.json -OutputDocx .\output\rendered\invoice.edited.docx -SummaryJson .\output\rendered\invoice.edit.summary.json -BuildDir build-codex-clang-compat -SkipBuild

To verify that the direct-edit output also looks right after Word renders it, run the focused visual regression. It creates the edited DOCX, exports the baseline and edited documents through Microsoft Word, renders PNG evidence, and writes a before/after contact sheet:

pwsh -ExecutionPolicy Bypass -File .\scripts\run_edit_document_from_plan_visual_regression.ps1 -BuildDir build-codex-clang-compat -SkipBuild
pwsh -ExecutionPolicy Bypass -File .\scripts\convert_render_data_to_patch_plan.ps1 -DataPath .\samples\chinese_invoice_template.render_data.json -MappingPath .\samples\chinese_invoice_template.render_data_mapping.json -OutputPatch .\output\rendered\invoice.generated.render_patch.json -SummaryJson .\output\rendered\invoice.generated.render_patch.summary.json

For faster feedback while editing mappings, run scripts/lint_render_data_mapping.ps1 before template export or rendering. It checks selector shape, duplicate targets inside each category, and optional business-data source paths without opening the .docx template.

pwsh -ExecutionPolicy Bypass -File .\scripts\lint_render_data_mapping.ps1 -MappingPath .\samples\chinese_invoice_template.render_data_mapping.json -DataPath .\samples\chinese_invoice_template.render_data.json -SummaryJson .\output\rendered\invoice.mapping.lint.summary.json

Before rendering, you can validate the whole template + mapping + data contract with scripts/validate_render_data_mapping.ps1. The wrapper exports a draft render plan, converts business data into a patch, and replays that patch onto the draft so missing mappings, invalid source paths, duplicate targets, or leftover placeholders fail before the final .docx step.

pwsh -ExecutionPolicy Bypass -File .\scripts\validate_render_data_mapping.ps1 -InputDocx .\samples\chinese_invoice_template.docx -MappingPath .\samples\chinese_invoice_template.render_data_mapping.json -DataPath .\samples\chinese_invoice_template.render_data.json -SummaryJson .\output\rendered\invoice.validation.summary.json -DraftPlanOutput .\output\rendered\invoice.validation.draft.render-plan.json -GeneratedPatchOutput .\output\rendered\invoice.validation.generated.render_patch.json -PatchedPlanOutput .\output\rendered\invoice.validation.patched.render-plan.json -BuildDir build-codex-clang-compat -SkipBuild -RequireComplete

For the full .docx + business JSON + mapping = final .docx workflow, use scripts/render_template_document_from_data.ps1. It runs export, convert, patch, and render as one direct pipeline, keeps -RequireComplete semantics on the patch step by default, and still lets you preserve the generated patch plus draft / patched render plans when needed.

pwsh -ExecutionPolicy Bypass -File .\scripts\render_template_document_from_data.ps1 -InputDocx .\samples\chinese_invoice_template.docx -DataPath .\samples\chinese_invoice_template.render_data.json -MappingPath .\samples\chinese_invoice_template.render_data_mapping.json -OutputDocx .\output\rendered\invoice.from-data.docx -SummaryJson .\output\rendered\invoice.from-data.summary.json -PatchPlanOutput .\output\rendered\invoice.generated.render_patch.json -DraftPlanOutput .\output\rendered\invoice.draft.render-plan.json -PatchedPlanOutput .\output\rendered\invoice.filled.render-plan.json -BuildDir build-codex-clang-compat -SkipBuild

inspect-sections prints the current section count together with per-section header/footer attachment flags for default, first, and even references. The mutating commands save in place by default; pass --output <path> to write to a separate .docx. Pass --json to inspect-sections when you need the same section layout information in a machine-readable object. The mutating commands also accept --json and emit command, ok, in_place, sections, headers, and footers, plus command-specific fields such as section, source, target, part, and kind.

inspect-header-parts / inspect-footer-parts list loaded part indexes in the same order consumed by assign-section-* and remove-*-part. Their output includes each part's relationship id, package entry path, section references, and paragraph text. Pass --json when you need the same information as a machine-readable object.

Additional representative command groups:

# Paragraphs, runs, styles, and numbering
featherdoc_cli inspect-paragraphs input.docx --paragraph 4 --json
featherdoc_cli set-paragraph-style input.docx 4 Heading2 --output styled-paragraph.docx --json
featherdoc_cli clear-paragraph-style input.docx 4 --output cleared-paragraph-style.docx --json
featherdoc_cli set-run-style input.docx 4 1 Strong --output styled-run.docx --json
featherdoc_cli clear-run-style input.docx 4 1 --output cleared-run-style.docx --json
featherdoc_cli set-run-font-family input.docx 4 1 Consolas --output font-run.docx --json
featherdoc_cli clear-run-font-family input.docx 4 1 --output cleared-run-font.docx --json
featherdoc_cli set-run-language input.docx 4 1 en-US --output language-run.docx --json
featherdoc_cli clear-run-language input.docx 4 1 --output cleared-run-language.docx --json
featherdoc_cli inspect-default-run-properties input.docx --json
featherdoc_cli set-default-run-properties input.docx --font-family "Segoe UI" --east-asia-font-family "Microsoft YaHei" --language en-US --east-asia-language zh-CN --rtl true --output default-run-properties.docx --json
featherdoc_cli clear-default-run-properties input.docx --primary-language --rtl --output cleared-default-run-properties.docx --json
featherdoc_cli inspect-style-run-properties input.docx Normal --json
featherdoc_cli materialize-style-run-properties input.docx Normal --output materialized-style-run-properties.docx --json
featherdoc_cli set-style-run-properties input.docx Normal --font-family "Segoe UI" --east-asia-font-family "Microsoft YaHei" --language en-US --east-asia-language zh-CN --rtl true --paragraph-bidi true --output style-run-properties.docx --json
featherdoc_cli clear-style-run-properties input.docx Normal --primary-language --rtl --paragraph-bidi --output cleared-style-run-properties.docx --json
featherdoc_cli inspect-style-inheritance input.docx Normal --json
featherdoc_cli inspect-paragraph-style-properties input.docx Heading1 --json
featherdoc_cli set-paragraph-style-properties input.docx Heading1 --next-style BodyText --outline-level 1 --output updated-paragraph-style-properties.docx --json
featherdoc_cli clear-paragraph-style-properties input.docx Heading1 --next-style --outline-level --output cleared-paragraph-style-properties.docx --json
featherdoc_cli rebase-character-style-based-on input.docx ReviewStrong Strong --output rebased-character-style.docx --json
featherdoc_cli rebase-paragraph-style-based-on input.docx Heading2 Normal --output rebased-paragraph-style.docx --json
featherdoc_cli ensure-paragraph-style input.docx ReviewHeading --name "Review Heading" --based-on Heading1 --output ensured-paragraph-style.docx --json
featherdoc_cli ensure-character-style input.docx ReviewStrong --name "Review Strong" --based-on Strong --output ensured-character-style.docx --json
featherdoc_cli ensure-numbering-definition input.docx --definition-name OutlineReview --numbering-level 0:decimal:1:%1. --output numbering.docx --json
featherdoc_cli ensure-style-linked-numbering input.docx --definition-name HeadingReview --numbering-level 0:decimal:1:%1. --numbering-level 1:decimal:1:%1.%2. --style-link Heading1:0 --style-link Heading2:1 --output linked-style-numbering.docx --json
featherdoc_cli set-paragraph-numbering input.docx 6 --definition 12 --level 0 --output numbered.docx --json
featherdoc_cli set-paragraph-style-numbering input.docx Heading2 --definition-name HeadingReview --numbering-level 0:decimal:1:%1. --style-level 1 --output style-numbering.docx --json
featherdoc_cli clear-paragraph-style-numbering input.docx Heading2 --output cleared-style-numbering.docx --json
featherdoc_cli set-paragraph-list input.docx 6 --kind bullet --level 1 --output bulleted.docx --json
featherdoc_cli restart-paragraph-list input.docx 10 --kind decimal --level 0 --output restarted-list.docx --json
featherdoc_cli clear-paragraph-list input.docx 10 --output cleared-list.docx --json

# Template inspection and bookmark-driven edits
featherdoc_cli inspect-template-paragraphs input.docx --part header --index 0 --paragraph 0 --json
featherdoc_cli inspect-template-tables input.docx --part body --table 0 --json
featherdoc_cli inspect-template-table-rows input.docx 0 --row 1 --json
featherdoc_cli inspect-template-table-cells input.docx 0 --row 1 --cell 1 --json
featherdoc_cli replace-bookmark-text input.docx customer_name --text "Ada Lovelace" --output bookmark-text.docx --json
featherdoc_cli fill-bookmarks input.docx --set customer_name "Ada Lovelace" --set invoice_no INV-001 --output filled.docx --json
featherdoc_cli fill-bookmarks input.docx --set-file customer_name customer_name.txt --set-file invoice_no invoice_no.txt --output filled-from-files.docx --json
featherdoc_cli replace-bookmark-paragraphs input.docx notes --paragraph "Line one" --paragraph "Line two" --output bookmark-paragraphs.docx --json
featherdoc_cli replace-bookmark-paragraphs input.docx notes --paragraph-file note-1.txt --paragraph-file note-2.txt --output bookmark-paragraphs-files.docx --json
featherdoc_cli replace-bookmark-table input.docx line_items --row "SKU-1" --cell "2" --cell "$10" --output bookmark-table.docx --json
featherdoc_cli replace-bookmark-table-rows input.docx line_items --row "SKU-2" --cell "4" --cell "$20" --output bookmark-table-rows.docx --json
featherdoc_cli replace-bookmark-table-rows input.docx line_items --row-file sku.txt --cell-file qty.txt --cell-file price.txt --output bookmark-table-rows-files.docx --json
featherdoc_cli remove-bookmark-block input.docx optional_section --output bookmark-block-removed.docx --json
featherdoc_cli set-bookmark-block-visibility input.docx optional_section --visible false --output bookmark-hidden.docx --json
featherdoc_cli apply-bookmark-block-visibility input.docx --hide optional_section --show totals --output bookmark-visibility.docx --json

# Images and page fields
featherdoc_cli replace-bookmark-image input.docx logo assets/logo.png --width 120 --height 40 --output bookmark-image.docx --json
featherdoc_cli replace-bookmark-floating-image input.docx hero assets/hero.png --width 320 --height 180 --horizontal-reference margin --vertical-reference paragraph --wrap-mode square --output bookmark-floating-image.docx --json
featherdoc_cli extract-image input.docx exported.png --relationship-id rId5 --json
featherdoc_cli replace-image input.docx replacement.png --relationship-id rId5 --output image-replaced.docx --json
featherdoc_cli remove-image input.docx --relationship-id rId5 --output image-removed.docx --json
featherdoc_cli append-image input.docx badge.png --width 96 --height 48 --output image-appended.docx --json
featherdoc_cli append-total-pages-field input.docx --part section-footer --section 1 --kind first --output total-pages.docx --json

assign-section-header / assign-section-footer make a section reuse an already loaded header/footer part by index. remove-section-header / remove-section-footer detach one section-level reference kind without removing the underlying part if it is still used elsewhere. remove-header-part / remove-footer-part drop one loaded part entirely and detach every section reference that points at it. move-header-part / move-footer-part reorder the loaded part indexes while keeping section references bound to the same underlying relationship ids.

show-section-header / show-section-footer print the referenced paragraphs one line per paragraph. set-section-header / set-section-footer rewrite the target part as plain paragraphs from --text or --text-file, and create the requested section reference automatically when it does not exist yet. show-section-header / show-section-footer also accept --json, which emits part, section, kind, present, and paragraphs fields for scriptable inspection.

The short block above is intentionally representative rather than exhaustive. For the full CLI surface, including template-table row/cell editing, style and numbering inspection, image inspection and replacement, page setup mutation, and page-number field insertion, see docs/index.rst.

Install

cmake --install build --prefix install

The installed package now also carries repository-facing metadata, visual-validation preview assets, repro guides, release-artifact templates, and legal files under share/FeatherDoc, including:

  • CHANGELOG.md
  • README.md
  • README.zh-CN.md
  • RELEASE_ARTIFACT_TEMPLATE.md
  • RELEASE_ARTIFACT_TEMPLATE.zh-CN.md
  • VISUAL_VALIDATION_QUICKSTART.md
  • VISUAL_VALIDATION_QUICKSTART.zh-CN.md
  • VISUAL_VALIDATION.md
  • VISUAL_VALIDATION.zh-CN.md
  • visual-validation/
  • LICENSE
  • LICENSE.upstream-mit
  • NOTICE
  • LEGAL.md

VISUAL_VALIDATION_QUICKSTART*.md is the shortest screenshot -> command -> review-task entry when you are browsing share/FeatherDoc directly. VISUAL_VALIDATION*.md then expands the same preview PNGs back to the source-checkout commands that regenerate them and to the review-task wrappers that consume the resulting evidence. RELEASE_ARTIFACT_TEMPLATE*.md gives a copy-paste release-body skeleton that points back to the installed previews, repro commands, and preflight evidence files.

If you only need the one-shot commands, copy these:

pwsh -ExecutionPolicy Bypass -File <repo-root>\scripts\run_word_visual_release_gate.ps1
pwsh -ExecutionPolicy Bypass -File <repo-root>\scripts\run_release_candidate_checks.ps1
pwsh -ExecutionPolicy Bypass -File <repo-root>\scripts\open_latest_word_review_task.ps1
pwsh -ExecutionPolicy Bypass -File <repo-root>\scripts\open_latest_fixed_grid_review_task.ps1 -PrintPrompt
pwsh -ExecutionPolicy Bypass -File <repo-root>\scripts\sync_latest_visual_review_verdict.ps1

To validate the installed package from a clean external CMake consumer, run:

pwsh -ExecutionPolicy Bypass -File .\scripts\run_install_find_package_smoke.ps1 `
    -BuildDir build-msvc-nmake `
    -InstallDir build-msvc-install `
    -ConsumerBuildDir build-msvc-install-consumer `
    -Generator "NMake Makefiles" `
    -Config Release

This is the same install + find_package smoke path that the Windows CI now uses after the main build, tests, and sample runs.

A local run_release_candidate_checks.ps1 execution also writes output/release-candidate-checks/START_HERE.md; use that summary-root note as the first entry before you open report/ARTIFACT_GUIDE.md or REVIEWER_CHECKLIST.md.

If you also want release-preflight to gate a template DOCX against a committed schema baseline, pass -TemplateSchemaInputDocx, -TemplateSchemaBaseline, and one of -TemplateSchemaSectionTargets / -TemplateSchemaResolvedSectionTargets. The wrapper then records that check in report/summary.json, including whether the committed schema is lint-clean, and fails the whole preflight on either schema drift or schema-maintenance issues.

If you want the same preflight to gate every repository-registered template schema baseline at once, pass -TemplateSchemaManifestPath instead. The wrapper then runs check_template_schema_manifest.ps1, records the manifest status plus entry/drift/dirty-baseline counts in report/summary.json, and fails the whole preflight when any registered baseline drifts or any committed schema baseline needs repair.

If you also want release-preflight to exercise a real-template regression pack, pass -ProjectTemplateSmokeManifestPath. The wrapper then runs run_project_template_smoke.ps1, records the manifest path, summary path, entry counts, failed-entry count, and aggregated project-template visual_verdict in report/summary.json, and threads the same status through START_HERE.md, ARTIFACT_GUIDE.md, REVIEWER_CHECKLIST.md, and release_handoff.md. Add -ProjectTemplateSmokeRequireFullCoverage when the same preflight should also fail on any tracked .docx / .dotx candidate that is neither registered in the smoke manifest nor listed under candidate_exclusions; the wrapper writes the full scan to project-template-smoke/candidate_discovery.json and surfaces the registered/unregistered/excluded counts in the release bundle.

When you want to add or refresh a repository-level baseline without manually editing baselines/template-schema/manifest.json, prefer register_template_schema_manifest_entry.ps1. It prepares a generated fixture when needed, freezes the normalized schema baseline, and writes or updates the matching manifest entry in one step. Before writing the manifest, it now runs the same baseline gate used by CI, so dirty schemas or document/schema drift are rejected at registration time unless you explicitly pass -SkipBaselineCheck.

The same workflow now also uploads a separate windows-msvc-release-metadata artifact containing build-msvc-install/share/FeatherDoc/**, a root-level RELEASE_METADATA_START_HERE.md, and output/release-candidate-checks-ci/START_HERE.md plus output/release-candidate-checks-ci/report/**, including the generated ARTIFACT_GUIDE.md, REVIEWER_CHECKLIST.md, release_handoff.md, release_body.zh-CN.md, and release_summary.zh-CN.md. Open RELEASE_METADATA_START_HERE.md first, then continue into START_HERE.md, ARTIFACT_GUIDE.md, and REVIEWER_CHECKLIST.md. That cloud artifact intentionally keeps the visual gate skipped; the final screenshot-backed Word verdict still belongs to the local Windows preflight path.

After a later local screenshot review updates the task verdicts, run pwsh -ExecutionPolicy Bypass -File .\scripts\sync_latest_visual_review_verdict.ps1 for the shortest auto-detected path. If you need to override paths manually, keep using sync_visual_review_verdict.ps1 with explicit -GateSummaryJson / -ReleaseCandidateSummaryJson arguments. Both routes can promote the final Word verdict into the saved summaries and refresh START_HERE.md, the guide/checklist, and the release-facing notes without rerunning the full preflight. Those refreshed entry points now surface not only the top-level visual verdict, but also the section page setup verdict, the page number fields verdict, every curated visual-regression bundle verdict, and the matching review-task paths / bundle-specific open_latest_word_review_task.ps1 -SourceKind <bundle-key>-visual-regression-bundle shortcuts.

Use From CMake

list(APPEND CMAKE_PREFIX_PATH "/path/to/FeatherDoc/install")
find_package(FeatherDoc CONFIG REQUIRED)

add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE FeatherDoc::FeatherDoc)

The generated package config also exposes:

  • FeatherDoc_VERSION
  • FeatherDoc_DESCRIPTION
  • FeatherDoc_PACKAGE_DATA_DIR

Quick Start

#include <featherdoc.hpp>
#include <iostream>

int main() {
    featherdoc::Document doc("file.docx");
    if (const auto error = doc.open()) {
        const auto& error_info = doc.last_error();
        std::cerr << error.message();
        if (!error_info.detail.empty()) {
            std::cerr << ": " << error_info.detail;
        }
        if (!error_info.entry_name.empty()) {
            std::cerr << " [entry=" << error_info.entry_name << ']';
        }
        if (error_info.xml_offset.has_value()) {
            std::cerr << " [xml_offset=" << *error_info.xml_offset << ']';
        }
        std::cerr << '\n';
        return 1;
    }

    for (auto paragraph : doc.paragraphs()) {
        std::string text;
        for (auto run : paragraph.runs()) {
            text += run.get_text();
        }
        std::cout << text << '\n';
    }

    for (auto table : doc.tables()) {
        for (auto row : table.rows()) {
            for (auto cell : row.cells()) {
                for (auto paragraph : cell.paragraphs()) {
                    std::string text;
                    for (auto run : paragraph.runs()) {
                        text += run.get_text();
                    }
                    std::cout << text << '\n';
                }
            }
        }
    }

    return 0;
}

Run represents WordprocessingML text runs, not whole lines. If one logical line is split into multiple runs, concatenate run.get_text() values inside a paragraph before printing. Text inside tables is traversed through doc.tables() -> rows() -> cells() -> paragraphs().

Find APIs By Task

If you mainly enter the project through the English README, start from the job you want to get done:

  • Create, open, save, or diagnose a document: create_empty(), open(), save(), save_as(), path(), last_error()
  • Edit body text: paragraphs(), runs(), set_text(), plus paragraph/run insertion and removal helpers
  • Work with tables and layout: append_table(), row/column insertion, merge_right(), merge_down(), unmerge_*(), fixed-grid width APIs, and layout-mode helpers
  • Fill templates or validate bookmarks: list_bookmarks(), validate_template(), fill_bookmarks(), replace_bookmark_with_*(), body_template(), header_template(), footer_template()
  • Work with images and page-number fields: append_image(), append_floating_image(), replace_*image(), append_page_number_field(), append_total_pages_field()
  • Work with sections, headers/footers, and page setup: inspect_sections(), get_section_page_setup(), set_section_page_setup(), ensure_*header*(), ensure_*footer*(), append_section(), insert_section(), move_section(), move_header_part(), move_footer_part()
  • Work with styles, numbering, and language metadata: list_styles(), find_style(), ensure_*style(), ensure_numbering_definition(), ensure_style_linked_numbering(), set_paragraph_style_numbering(), default_run_*(), style_run_*(), resolve_style_properties(), materialize_style_run_properties(), rebase_character_style_based_on(), set_paragraph_style_based_on(), set_paragraph_style_next_style(), set_paragraph_style_outline_level(), rebase_paragraph_style_based_on()
  • Prefer the CLI for scriptable inspection or one-shot rewrites: inspect-*, validate-template, append-page-number-field, set-section-page-setup

For fuller parameter details, runnable samples, and edge-case notes, continue into docs/index.rst; its Task-Oriented API Map and Task-Oriented Sample And CLI Map mirror the same entry points in more detail.

Use Paragraph::set_text(...) when you want to replace one paragraph's body content in place while preserving paragraph-level properties such as style or bidirectional settings. Use Paragraph::insert_paragraph_before(...) or insert_paragraph_after(...) when you need to add a sibling paragraph around an existing anchor paragraph, insert_paragraph_like_before() / insert_paragraph_like_after() when you want the new paragraph to inherit the anchor paragraph's paragraph-level properties, Run::insert_run_before(...) / insert_run_after(...) when you need to add sibling runs around an existing anchor run, Run::insert_run_like_before() / insert_run_like_after() to clone the anchor run's formatting into a new empty sibling run, Run::remove() to drop one run from a paragraph, and Paragraph::remove() when you want to delete one paragraph without leaving an invalid container behind. Paragraph::remove() intentionally refuses to remove section-break paragraphs or the last required paragraph inside a body, header, footer, or table cell container.

auto paragraph = doc.paragraphs();
paragraph.set_text("anchor");

auto prepended = paragraph.insert_paragraph_before("Lead-in");
prepended.add_run(" note");

auto cloned = paragraph.insert_paragraph_like_after();
cloned.set_text("Another paragraph with the same paragraph style");

auto anchor = paragraph.runs();
anchor.insert_run_before("left ", featherdoc::formatting_flag::bold);
anchor.insert_run_after(" right");

auto cloned_run = anchor.insert_run_like_before();
cloned_run.set_text("styled clone ");

auto removable_run = paragraph.add_run(" temporary");
removable_run.remove();

auto removable_paragraph = paragraph.insert_paragraph_after("Delete me");
removable_paragraph.remove();

For a runnable "edit an existing document and save it back" example, build featherdoc_sample_edit_existing from samples/sample_edit_existing.cpp. FeatherDoc already supports opening an existing .docx, mutating paragraphs, runs, table cells, inline body images, headers, footers, and bookmark-backed template regions, and saving the result back to disk. For a focused "reopen and replace existing header/footer images" example, build featherdoc_sample_edit_existing_part_images from samples/sample_edit_existing_part_images.cpp. For a focused "reopen and append body/header/footer paragraphs through TemplatePart handles" example, build featherdoc_sample_edit_existing_part_paragraphs from samples/sample_edit_existing_part_paragraphs.cpp. For a focused "reopen and insert body/header/footer paragraphs before existing anchor paragraphs" example, build featherdoc_sample_insert_paragraph_before from samples/sample_insert_paragraph_before.cpp. For a focused "reopen and clone paragraph formatting around existing body/header/footer anchor paragraphs" example, build featherdoc_sample_insert_paragraph_like_existing from samples/sample_insert_paragraph_like_existing.cpp. For a focused "reopen and insert runs around existing body/header/footer anchor runs" example, build featherdoc_sample_insert_run_around_existing from samples/sample_insert_run_around_existing.cpp. For a focused "reopen and clone run formatting around existing body/header/footer anchor runs" example, build featherdoc_sample_insert_run_like_existing from samples/sample_insert_run_like_existing.cpp. For a focused "reopen and append new images to existing body/header/footer parts" example, build featherdoc_sample_edit_existing_part_append_images from samples/sample_edit_existing_part_append_images.cpp.

Use append_table(row_count, column_count) when you need to create a new body table programmatically. The returned Table can be extended with append_row(), widened with append_cell(), or used as an anchor for insert_table_before(...) / insert_table_after(...) when you need to insert new sibling tables around an existing one. Use insert_table_like_before() / insert_table_like_after() when you want to clone the current table's layout and formatting into a new empty sibling table.

auto table = doc.append_table(2, 2);

auto first_row = table.rows();
auto first_cell = first_row.cells();
first_cell.set_text("r0c0");
first_cell.next();
first_cell.set_text("r0c1");

auto extra_row = table.append_row();
auto extra_cell = extra_row.cells();
extra_cell.set_text("tail");
extra_row.append_cell().set_text("tail-2");

auto inserted = table.insert_table_after(1, 2);
inserted.rows().cells().set_text("inserted");

Use Table::set_width_twips(...), set_column_width_twips(...), clear_column_width(), set_style_id(...), set_border(...), set_layout_mode(...), set_alignment(...), set_indent_twips(...), set_cell_spacing_twips(...), set_cell_margin_twips(...), and set_style_look(...) alongside TableCell::set_text(...), get_text(), set_width_twips(...), Table::remove(), insert_table_before(), insert_table_after(), insert_table_like_before(), insert_table_like_after(), TableCell::remove(), insert_cell_before(), insert_cell_after(), TableRow::remove(), insert_row_before(), insert_row_after(), merge_right(...), merge_down(...), unmerge_right(), unmerge_down(), set_vertical_alignment(...), set_border(...), set_fill_color(...), and set_margin_twips(...) when you need lightweight table layout editing without dropping down to raw WordprocessingML. width_twips() reports an explicit dxa width when present, column_width_twips(column_index) reports the current w:tblGrid width for one grid column, style_id() reports the current table style reference, style_look() reports the current first/last row or column emphasis together with row/column banding flags, layout_mode() reports the current auto-fit mode, alignment() / indent_twips() report table placement, cell_spacing_twips() reports inter-cell spacing, and cell_margin_twips() reports per-edge default cell margins, height_twips() / height_rule() report the current row height override, cant_split() reports whether Word keeps the row on one page, repeats_header() reports whether a row repeats as a table header, Table::remove() deletes one table while refusing to leave the parent container without the required block content and retargets the wrapper to the next surviving table when possible (otherwise the previous one). TableCell::remove() deletes one unmerged column across the whole table while refusing to remove the last remaining column, refusing any column that intersects a horizontal merge span, and retargeting the wrapper to the next surviving cell when possible (otherwise the previous one). When the table uses explicit w:tblGrid widths, removal also drops the matching w:gridCol entry so the deleted column's saved grid width disappears with it. TableCell::insert_cell_before() and insert_cell_after() clone the target column structure across every row, expand w:tblGrid, clear copied w:gridSpan / w:vMerge markup on the inserted cells, copy the source-side w:gridCol width into the inserted grid column, and refuse insertion boundaries that would land inside a horizontal merge span. On tables that are already fixed-layout, or that later switch to fixed-layout, with explicit widths for every w:gridCol, set_layout_mode(featherdoc::table_layout_mode::fixed), set_column_width_twips(...), TableCell::merge_right(), unmerge_right(), TableCell::insert_cell_before(), insert_cell_after(), and TableCell::remove() also normalize each cell's w:tcW to match the grid, while clear_column_width() drops w:tcW from any cell that still covers the cleared grid column so stale width hints do not linger. Switching to autofit or calling clear_layout_mode() does not erase the saved w:gridCol / w:tcW widths; it only changes the layout algorithm Word uses when rendering the table. Likewise, set_width_twips(...) and clear_width() only rewrite w:tblW; they do not rescale or discard the saved w:gridCol / w:tcW widths. When a table later re-runs fixed-grid normalization, any manual TableCell::set_width_twips(...) / clear_width() edits on cells covered by that complete grid are overwritten back to the summed w:gridCol widths. For reopened legacy fixed-layout tables whose saved w:tcW no longer matches w:gridCol, reapplying set_layout_mode(featherdoc::table_layout_mode::fixed) forces that same normalization pass. The same normalization also runs when those reopened fixed-layout tables go through explicit span or column edits such as merge_right(), unmerge_right(), set_column_width_twips(...), insert_cell_before() / insert_cell_after(), or TableCell::remove(). clear_column_width() follows the same reopened fixed-layout path for the cleared grid column, but only clears w:tcW on cells that still cover that column instead of re-normalizing unrelated stale cell widths. Plain non-grid edits such as TableCell::set_text(...), set_fill_color(...), and set_border(...) do not trigger that normalization on their own. The same is true for row-only structural edits: TableRow::insert_row_before(), insert_row_after(), and remove() keep reopened stale w:tcW values as-is instead of re-normalizing them from w:tblGrid. unmerge_right() splits one pure horizontal merged cell back into standalone cells in the current row while keeping the anchor text in the leftmost cell and leaving restored siblings empty for later editing. insert_table_before() and insert_table_after() create a new empty sibling table directly before or after the selected table and retarget the wrapper to the inserted table. insert_table_like_before() and insert_table_like_after() clone the selected table's structure plus table/row/cell formatting into a new sibling table, clear the cloned cell content, and retarget the wrapper to the inserted table. TableRow::remove() deletes one row while refusing to remove the last remaining row, insert_row_before() and insert_row_after() clone the current row structure into a new empty row directly before or after it while refusing rows that participate in vertical merge chains, and unmerge_down() removes one whole vertical merge chain even when the current wrapper points at a continuation cell, leaving the restored lower cells empty until the caller writes new content. column_span() reports the current horizontal span. TableCell::set_text(...) replaces one cell's body with a single paragraph while preserving cell-level properties such as shading, margins, borders, and explicit width.

auto table = doc.append_table(1, 3);
table.set_width_twips(7200);
table.set_column_width_twips(0, 1800);
table.set_column_width_twips(1, 2400);
table.set_column_width_twips(2, 3000);
table.set_style_id("TableGrid");
table.set_style_look({true, false, false, false, true, false});
table.set_layout_mode(featherdoc::table_layout_mode::fixed);
table.set_alignment(featherdoc::table_alignment::center);
table.set_indent_twips(240);
table.set_cell_spacing_twips(120);
table.set_cell_margin_twips(featherdoc::cell_margin_edge::left, 96);
table.set_cell_margin_twips(featherdoc::cell_margin_edge::right, 96);
table.set_border(featherdoc::table_border_edge::inside_vertical,
                 {featherdoc::border_style::single, 8, "808080", 0});

auto row = table.rows();
row.set_height_twips(360, featherdoc::row_height_rule::exact);
row.set_cant_split();
row.set_repeats_header();
auto cell = row.cells();

cell.set_width_twips(2400);
cell.set_vertical_alignment(featherdoc::cell_vertical_alignment::center);
cell.set_fill_color("D9EAF7");
cell.set_margin_twips(featherdoc::cell_margin_edge::left, 120);
cell.set_margin_twips(featherdoc::cell_margin_edge::right, 120);
cell.paragraphs().add_run("Merged title");
cell.merge_right(1);
cell.set_border(featherdoc::cell_border_edge::bottom,
                {featherdoc::border_style::thick, 12, "000000", 0});

auto next_row = table.append_row(3);
auto merged_column = next_row.cells();
merged_column.paragraphs().add_run("Below");
cell = row.cells();
cell.merge_down(1);

auto inserted_row = row.insert_row_after();
inserted_row.cells().set_text("Inserted");

auto inserted_before = next_row.insert_row_before();
inserted_before.cells().set_text("Inserted above");

std::cout << cell.column_span() << '\n'; // 2

style_look() only changes the style-routing flags stored in w:tblLook, so visible differences depend on the table style definition present in the document. It is useful when you want to keep the same style id but switch which rows or columns that style treats as emphasized or banded.

For runnable insertion examples, build featherdoc_sample_insert_table_row from samples/sample_insert_table_row.cpp for the insert_row_after() flow, or featherdoc_sample_insert_table_row_before from samples/sample_insert_table_row_before.cpp for the insert_row_before() flow. Both samples create a seed table, reopen the saved .docx, insert a cloned row in the middle, and write the updated result back out.

For a runnable column-insertion example, build featherdoc_sample_insert_table_column from samples/sample_insert_table_column.cpp. It seeds a two-column table, reopens the saved .docx, inserts one cloned column after the first column and another before the result column, and writes the updated four-column result back out.

For a runnable unmerge example, build featherdoc_sample_unmerge_table_cells from samples/sample_unmerge_table_cells.cpp. It creates one horizontal merge and one vertical merge, reopens the saved .docx, splits them back into standalone cells, and continues editing the restored cells.

For a runnable table-removal example, build featherdoc_sample_remove_table from samples/sample_remove_table.cpp. It creates three body tables, reopens the saved .docx, removes the temporary middle table, and continues editing the following table through the same wrapper. For a runnable table-column removal example, build featherdoc_sample_remove_table_column from samples/sample_remove_table_column.cpp. It creates a three-column body table, reopens the saved .docx, removes the temporary middle column, and continues editing the surviving result column through the same cell wrapper. For a runnable table-insertion example, build featherdoc_sample_insert_table_around_existing from samples/sample_insert_table_around_existing.cpp. It reopens a saved .docx, inserts new tables directly before and after an existing anchor table, and continues editing the surrounding tables through the returned wrappers. For a runnable styled-table cloning example, build featherdoc_sample_insert_table_like_existing from samples/sample_insert_table_like_existing.cpp. It reopens a saved .docx, duplicates an existing table's layout and styling into new empty sibling tables, fills the clones, and keeps the original anchor table editable. For a runnable table-spacing edit example, build featherdoc_sample_edit_existing_table_spacing from samples/sample_edit_existing_table_spacing.cpp. It reopens a saved .docx, adds tblCellSpacing to an existing table, and makes the new gutters visible in Word's rendered output. For a runnable table-column-width edit example, build featherdoc_sample_edit_existing_table_column_widths from samples/sample_edit_existing_table_column_widths.cpp. It reopens a saved .docx, rewrites w:tblGrid column widths on an existing table, and makes the narrow/medium/wide column split visible in Word's rendered output. For a runnable table-style-look edit example, build featherdoc_sample_edit_existing_table_style_look from samples/sample_edit_existing_table_style_look.cpp. It reopens a saved .docx, updates tblLook on an existing table, and keeps the original table style reference in place.

Use append_image(path) to append an inline image at its intrinsic pixel size, or append_image(path, width_px, height_px) when you want explicit scaling. These APIs are available on both Document and TemplatePart, so you can append images to the main body or to an existing body/header/footer part wrapper. The current image support is limited to .png, .jpg, .jpeg, .gif, and .bmp.

doc.append_image("logo.png");
doc.append_image("badge.png", 96, 48);

auto header_template = doc.section_header_template(0);
header_template.append_image("header-logo.png", 144, 48);

Use append_floating_image(path, options) or append_floating_image(path, width_px, height_px, options) when you want an anchored wp:anchor image with explicit margin/page-relative offsets. floating_image_options currently lets you pick horizontal/vertical reference frames, pixel offsets, whether the image sits behind text, whether overlap is allowed, the anchor z-order, rectangular wrap modes plus wrap distances, and basic crop values. The same API works on Document and TemplatePart.

featherdoc::floating_image_options options;
options.horizontal_reference =
    featherdoc::floating_image_horizontal_reference::margin;
options.horizontal_offset_px = 460;
options.vertical_reference =
    featherdoc::floating_image_vertical_reference::margin;
options.vertical_offset_px = 24;
options.z_order = 32;
options.wrap_mode = featherdoc::floating_image_wrap_mode::square;
options.wrap_distance_left_px = 12;
options.wrap_distance_right_px = 12;
options.crop = featherdoc::floating_image_crop{90U, 0U, 140U, 0U};

doc.append_floating_image("badge.png", 144, 48, options);

auto body_template = doc.body_template();
body_template.append_floating_image("stamp.png", 128, 48, options);

Use inline_images() to inspect inline images that already exist in the main document body. Each returned inline_image_info includes the image index, relationship id, media entry path, display name, content type, and rendered pixel size derived from wp:extent.

Use drawing_images() when you need the full picture list from an existing document part, including both wp:inline and wp:anchor drawings. Each returned drawing_image_info includes the same metadata plus a drawing_image_placement value so you can distinguish inline and anchored objects.

Use extract_drawing_image(index, path) to copy any existing drawing-backed image out of the .docx, and replace_drawing_image(index, path) to swap one inline or anchored image with a new file while preserving the current rendered size and placement XML. Use remove_drawing_image(index) to delete one inline or anchored drawing and drop its media part on the next save once no relationship still points at it.

const auto drawings = doc.drawing_images();
for (const auto& image : drawings) {
    if (image.placement == featherdoc::drawing_image_placement::anchored_object) {
        doc.remove_drawing_image(image.index);
    }
}

Use extract_inline_image(index, path) to copy one existing inline body image out of the .docx, and replace_inline_image(index, path) to swap one body image with a new file while preserving the current displayed size. remove_inline_image(index) deletes only inline images. Replacement retargets only the selected relationship. If the old media part becomes unreferenced afterwards, FeatherDoc removes it from the next saved archive.

const auto images = doc.inline_images();
if (!images.empty()) {
    doc.extract_inline_image(images[0].index, "first-image.bin");
    doc.remove_inline_image(images[0].index);
}

The same drawing_images(), extract_drawing_image(...), remove_drawing_image(...), replace_drawing_image(...), inline_images(), extract_inline_image(...), remove_inline_image(...), and replace_inline_image(...) APIs are also available on TemplatePart handles when you need existing body/header/footer drawings from already loaded parts.

For runnable visual samples, build featherdoc_sample_floating_images from samples/sample_floating_images.cpp when you want mixed inline/floating body drawings, or featherdoc_sample_remove_images from samples/sample_remove_images.cpp when you want a minimal existing-image removal workflow.

Use set_paragraph_list(paragraph, kind, level) to attach managed bullet or decimal numbering to a paragraph. Call restart_paragraph_list(paragraph, kind, level) when you want a fresh managed list sequence that starts over at the default marker for that kind, and clear_paragraph_list(paragraph) when you want to remove the list marker from that paragraph again.

auto item = doc.paragraphs();
doc.set_paragraph_list(item, featherdoc::list_kind::bullet);
item.add_run("first item");

auto nested = item.insert_paragraph_after("");
doc.set_paragraph_list(nested, featherdoc::list_kind::decimal, 1);
nested.add_run("nested item");

auto restarted = nested.insert_paragraph_after("");
doc.restart_paragraph_list(restarted, featherdoc::list_kind::decimal);
restarted.add_run("restarted item 1");

For a runnable list-restart example, build featherdoc_sample_restart_paragraph_list from samples/sample_restart_paragraph_list.cpp. It reopens a saved .docx, starts a second decimal list from 1., and keeps the restarted sequence consistent in Word's rendered output.

Use set_paragraph_style(paragraph, style_id) and set_run_style(run, style_id) to attach paragraph/run style references. When the source document does not already contain word/styles.xml, FeatherDoc creates a minimal styles part automatically. The generated catalog currently includes Normal, Heading1, Heading2, Quote, Emphasis, and Strong.

auto paragraph = doc.paragraphs();
doc.set_paragraph_style(paragraph, "Heading1");

auto styled_run = paragraph.add_run("Styled heading");
doc.set_run_style(styled_run, "Strong");

doc.clear_run_style(styled_run);
doc.clear_paragraph_style(paragraph);

Formatting Flags

paragraph.add_run("bold text", featherdoc::formatting_flag::bold);
paragraph.add_run(
    "mixed style",
    featherdoc::formatting_flag::bold |
        featherdoc::formatting_flag::italic |
        featherdoc::formatting_flag::underline
);

Document now uses std::filesystem::path, and both open() and save() return std::error_code so callers can inspect the failure reason directly.

if (const auto error = doc.save()) {
    const auto& error_info = doc.last_error();
    std::cerr << error.message() << '\n';
    std::cerr << error_info.detail << '\n';
    return 1;
}

last_error() exposes structured context for the most recent failure:

  • code: the same std::error_code returned by open() / save()
  • detail: richer human-readable explanation
  • entry_name: failing ZIP entry when relevant
  • xml_offset: parse offset for malformed XML

Use save_as(path) when you want to keep the current source document path unchanged and write the modified document to a different output file.

if (const auto error = doc.save_as("copy.docx")) {
    std::cerr << error.message() << '\n';
    return 1;
}

Use replace_bookmark_text(name, replacement) when you want to rewrite the content enclosed by a named bookmark range. The method returns the number of bookmark ranges replaced.

if (doc.replace_bookmark_text("customer_name", "FeatherDoc User") == 0) {
    std::cerr << "bookmark not found\n";
    return 1;
}

Use fill_bookmarks(...) when you want a first high-level template API for batch text filling. It accepts bookmark bindings, rewrites every matching bookmark range, and reports which requested fields were missing.

const auto result = doc.fill_bookmarks({
    {"customer_name", "FeatherDoc User"},
    {"invoice_no", "INV-2026-0001"},
    {"due_date", "2026-04-30"},
});

if (!result) {
    for (const auto& missing : result.missing_bookmarks) {
        std::cerr << "missing bookmark: " << missing << '\n';
    }
}

Use replace_bookmark_with_paragraphs(...) when a bookmark occupies its own paragraph and should expand into zero or more plain-text paragraphs. Passing an empty list removes the placeholder paragraph entirely.

doc.replace_bookmark_with_paragraphs(
    "line_items",
    {
        "Apple",
        "Pear",
        "Orange",
    });

Use remove_bookmark_block(...) when that same standalone bookmark paragraph should simply be deleted without constructing an empty replacement list.

doc.remove_bookmark_block("optional_note");

For a runnable example, build featherdoc_sample_remove_bookmark_block from samples/sample_remove_bookmark_block.cpp. It opens the Chinese invoice template, fills core fields, expands the item table, removes the standalone note_lines bookmark block, and saves the result for visual review.

Use replace_bookmark_with_table_rows(...) when a bookmark occupies its own paragraph inside a template table row and that row should expand into zero or more cloned rows. The template row's row/cell properties are preserved, while each generated cell body is rewritten to a single plain-text paragraph. Passing an empty list removes the template row entirely.

doc.replace_bookmark_with_table_rows(
    "line_item_row",
    {
        {"Apple", "2"},
        {"Pear", "5"},
        {"Orange", "1"},
    });

For a runnable Chinese business example, build featherdoc_sample_chinese_template from samples/sample_chinese_template.cpp. It opens samples/chinese_invoice_template.docx, fills Chinese customer fields, expands a bookmarked table row into a three-column quote table, and writes a finished output document.

Use replace_bookmark_with_table(...) when a bookmark occupies its own paragraph and should be replaced by a generated table block.

doc.replace_bookmark_with_table(
    "line_items",
    {
        {"Name", "Qty"},
        {"Apple", "2"},
        {"Pear", "5"},
    });

Use replace_bookmark_with_image(...) when a bookmark occupies its own paragraph and should become an inline image paragraph. The overload without dimensions uses the source image size; the second overload lets you scale it.

doc.replace_bookmark_with_image("company_logo", "logo.png");
doc.replace_bookmark_with_image("stamp", "stamp.png", 96, 48);

Use replace_bookmark_with_floating_image(...) when that standalone bookmark paragraph should instead become an anchored image paragraph. The floating placement uses the same floating_image_options struct as append_floating_image(...).

featherdoc::floating_image_options options;
options.horizontal_reference =
    featherdoc::floating_image_horizontal_reference::page;
options.horizontal_offset_px = 24;

doc.replace_bookmark_with_floating_image("callout", "badge.png", 144, 48,
                                         options);

Use body_template(), header_template(index), footer_template(index), section_header_template(section_index, kind), and section_footer_template(section_index, kind) when you want the same bookmark-based template APIs on an already loaded body/header/footer part. Each method returns a lightweight TemplatePart handle. A valid handle supports entry_name(), paragraphs(), append_paragraph(...), tables(), append_table(...), replace_bookmark_text(...), fill_bookmarks(...), replace_bookmark_with_paragraphs(...), remove_bookmark_block(...), replace_bookmark_with_table_rows(...), and replace_bookmark_with_table(...), replace_bookmark_with_image(...), replace_bookmark_with_floating_image(...), drawing_images(), extract_drawing_image(...), remove_drawing_image(...), replace_drawing_image(...), inline_images(), extract_inline_image(...), remove_inline_image(...), replace_inline_image(...), set_bookmark_block_visibility(...), and apply_bookmark_block_visibility(...). Missing section-specific references return an empty handle instead of creating a new part implicitly.

auto header_template = doc.section_header_template(0);
if (header_template) {
    auto note = header_template.append_paragraph("Header note");
    note.add_run(" added after opening the template part.");
    auto summary_table = header_template.append_table(2, 2);
    summary_table.rows().cells().set_text("Header cell");
    header_template.replace_bookmark_with_image("header_logo", "logo.png");
    header_template.replace_bookmark_with_table_rows(
        "line_item_row",
        {
            {"Apple", "2"},
            {"Pear", "5"},
        });
}

auto footer_template = doc.section_footer_template(0);
if (footer_template) {
    footer_template.fill_bookmarks({
        {"company_name", "Acme Corp"},
    });
    footer_template.remove_bookmark_block("optional_legal_notice");
    footer_template.replace_bookmark_with_paragraphs(
        "footer_lines",
        {
            "First line",
            "Second line",
        });
}

When you need one document-level contract for multiple parts at once, use Document::validate_template_schema(...) and group requirements by body, header, footer, or section-scoped header/footer targets.

const auto result = doc.validate_template_schema({
    {
        {featherdoc::template_schema_part_kind::section_header, std::nullopt, 0U,
         featherdoc::section_reference_kind::default_reference},
        {"header_title", featherdoc::template_slot_kind::text, true},
    },
    {
        {featherdoc::template_schema_part_kind::section_footer, std::nullopt, 0U,
         featherdoc::section_reference_kind::default_reference},
        {"footer_signature", featherdoc::template_slot_kind::text, true},
    },
});

For a runnable document-level sample, build featherdoc_sample_template_schema_validation from samples/sample_template_schema_validation.cpp. The CLI equivalent is featherdoc_cli validate-template-schema ..., which also accepts --schema-file <path> for reusable JSON schema contracts.

The schema-file flow accepts either compact string slots such as "header_title:text" or structured slot objects:

{
  "targets": [
    {
      "part": "section-header",
      "section": 0,
      "kind": "default",
      "slots": [
        { "bookmark": "header_title", "kind": "text" },
        { "bookmark_name": "header_note", "kind": "block", "required": true },
        { "bookmark": "header_rows", "kind": "table_rows", "count": 1 }
      ]
    }
  ]
}

A ready-to-run example lives at samples/template_schema_validation.schema.json.

If you do not want to hand-author the first schema, start from an existing template:

featherdoc_cli export-template-schema input.docx --output template-schema.json --json
featherdoc_cli validate-template-schema input.docx --schema-file template-schema.json --json

export-template-schema currently emits body plus loaded header[index] / footer[index] targets by default. Pass --section-targets when you want the same export rewritten as section-header / section-footer targets for direct section references. Pass --resolved-section-targets when you need the effective per-section header/footer view after following linked-to-previous references; that export also includes metadata such as resolved_from_section / linked_to_previous, and the JSON can still be fed back into validate-template-schema --schema-file .... All three modes serialize representable bookmark kinds using the same lightweight classification returned by list_bookmarks(). If you need a stable schema file for reviews or commits, run normalize-template-schema; if you need to compare two revisions, use diff-template-schema to get added / removed / changed targets directly. If you need to gate a committed schema in review or CI, use lint-template-schema; it returns 0 for clean schemas and 1 when it finds duplicate target identities, duplicate slot names, non-canonical ordering, or leaked entry_name metadata. If you want the safe canonical rewrite for those maintenance issues, run repair-template-schema; it strips entry_name, merges duplicate target identities using later-definition-wins semantics, folds duplicate slot names, and then normalizes target / slot order. If you need to compose a shared schema with a document-specific overlay, use merge-template-schema; later files upsert matching targets and replace same-bookmark slot definitions before the merged result is normalized. If you need to maintain a committed schema in place, use patch-template-schema with a patch file that can upsert_targets, remove_targets, remove_slots, and rename_slots; the patched result is normalized before it is printed or written. If you already have a reviewed left and right schema pair and want a reusable patch file, use build-template-schema-patch; it stays correctness-first, but when a target keeps the same full identity it prefers slot-level remove_slots, rename_slots, and partial upsert_targets. Only identity changes fall back to whole-target remove_targets plus upsert_targets, so applying the patch to the left schema still reproduces the normalized right schema. Add --fail-on-diff when you want the diff command to behave like a CI gate and return a non-zero exit code on schema drift. If you want a single command that exports, normalizes, compares, and gates against a committed baseline, use check-template-schema; it returns 0 on match and 1 on drift, and can optionally write the normalized generated schema with --output. If you prefer a higher-level repository entrypoint, the wrapper scripts scripts/freeze_template_schema_baseline.ps1 and scripts/check_template_schema_baseline.ps1 provide the same workflow with optional CLI auto-build/reuse logic. The baseline wrapper now runs lint-template-schema before the document-vs-baseline comparison and can write an optional repaired candidate with -RepairedSchemaOutput when the committed schema needs cleanup. scripts/check_template_schema_manifest.ps1 applies the same gate across the repository manifest, reports dirty_baseline_count in its summary.json, and can emit per-entry repaired candidates with -RepairedSchemaOutputDir.

A patch-template-schema patch file is a JSON object with zero or more of these arrays:

  • upsert_targets: regular exported schema targets, merged with the same target and slot-replacement rules as merge-template-schema
  • remove_targets: target selectors matched by full target identity, including part, index / part_index, section, kind, resolved_from_section, and linked_to_previous
  • remove_slots: the same selector fields plus bookmark or bookmark_name; matching slots are removed and empty targets are pruned
  • rename_slots: the same selector fields plus bookmark / bookmark_name for the old slot name and new_bookmark / new_bookmark_name for the new slot name
  • entry_name is ignored in selectors, so exported JSON can be copied into a patch file after trimming unrelated fields
  • {} is also valid and represents a no-op patch; that is what build-template-schema-patch emits when two schemas are already equivalent
  • build-template-schema-patch only emits rename_slots when the left/right target identity is unchanged and the old/new slot shapes match uniquely; if that confidence is missing, it falls back to remove/add style patch entries

Example patch file:

{
  "remove_targets": [
    {
      "part": "section-footer",
      "section": 1,
      "kind": "default"
    }
  ],
  "remove_slots": [
    {
      "part": "body",
      "bookmark": "summary_block"
    },
    {
      "part": "section-header",
      "section": 1,
      "kind": "default",
      "resolved_from_section": 0,
      "linked_to_previous": true,
      "bookmark_name": "legacy_header_note"
    }
  ],
  "rename_slots": [
    {
      "part": "section-header",
      "section": 1,
      "kind": "default",
      "resolved_from_section": 0,
      "linked_to_previous": true,
      "bookmark": "header_title",
      "new_bookmark": "document_title"
    }
  ],
  "upsert_targets": [
    {
      "part": "body",
      "slots": [
        {
          "bookmark": "customer",
          "kind": "text",
          "count": 2
        },
        {
          "bookmark": "invoice_no",
          "kind": "text"
        }
      ]
    }
  ]
}

append_paragraph(...) appends a new paragraph to the existing body/header/footer part and returns the appended Paragraph, so you can immediately continue editing it through add_run(...), set_text(...), or bidi-related paragraph APIs.

For a runnable existing-part table example, build featherdoc_sample_edit_existing_part_tables from samples/sample_edit_existing_part_tables.cpp. It creates a default header, adds three header tables, reopens the saved .docx, removes the temporary middle table, and continues editing the following header table through the same wrapper. For a runnable existing-part image-append example, build featherdoc_sample_edit_existing_part_append_images from samples/sample_edit_existing_part_append_images.cpp. It seeds body/header/ footer content, reopens the saved .docx, and appends new inline or floating images through TemplatePart handles before saving the edited result back out.

Use set_bookmark_block_visibility(name, visible) or apply_bookmark_block_visibility(...) when a bookmark pair should guard an optional block of sibling content such as paragraphs or tables. The template must place w:bookmarkStart in its own paragraph, w:bookmarkEnd in a later paragraph, and both marker paragraphs must share the same parent container. When visible is true, FeatherDoc keeps the content between them and removes only the marker paragraphs. When visible is false, FeatherDoc removes the whole block including both marker paragraphs.

const auto visibility = doc.apply_bookmark_block_visibility({
    {"promo_block", false},
    {"legal_block", true},
});

if (!visibility) {
    for (const auto& missing : visibility.missing_bookmarks) {
        std::cerr << "missing block bookmark: " << missing << '\n';
    }
}

Use header_count(), footer_count(), header_paragraphs(index), and footer_paragraphs(index) when you need to read or edit paragraphs stored in existing header/footer parts.

for (std::size_t i = 0; i < doc.header_count(); ++i) {
    for (auto paragraph = doc.header_paragraphs(i); paragraph.has_next();
         paragraph.next()) {
        for (auto run = paragraph.runs(); run.has_next(); run.next()) {
            std::cout << run.get_text() << '\n';
        }
    }
}

Use ensure_header_paragraphs() and ensure_footer_paragraphs() when you need to create and attach a default header/footer to the document's body-level section properties before editing it.

auto header = doc.ensure_header_paragraphs();
header.add_run("Generated header");

auto footer = doc.ensure_footer_paragraphs();
footer.add_run("Page 1");

Use section_count(), section_header_paragraphs(section_index, kind), and section_footer_paragraphs(section_index, kind) when you need to resolve the existing header/footer reference attached to a specific section.

for (std::size_t i = 0; i < doc.section_count(); ++i) {
    auto header = doc.section_header_paragraphs(i);
    if (header.has_next()) {
        std::cout << header.runs().get_text() << '\n';
    }
}

Use ensure_section_header_paragraphs(section_index, kind) and ensure_section_footer_paragraphs(section_index, kind) when you need to create and attach a missing section-specific header/footer reference before editing it. When kind is first_page or even_page, FeatherDoc also enables the required WordprocessingML switches (w:titlePg or word/settings.xml -> w:evenAndOddHeaders) automatically.

auto even_header = doc.ensure_section_header_paragraphs(
    1, featherdoc::section_reference_kind::even_page);
even_header.add_run("Even page header");

auto first_footer = doc.ensure_section_footer_paragraphs(
    1, featherdoc::section_reference_kind::first_page);
first_footer.add_run("First page footer");

Use assign_section_header_paragraphs(section_index, header_index, kind) and assign_section_footer_paragraphs(section_index, footer_index, kind) when you need multiple sections to reuse an existing header/footer part instead of creating a new one. Each call only rebinds the requested kind, so reuse across multiple kinds on the same section needs one call per kind.

auto shared_header = doc.assign_section_header_paragraphs(1, 0);
shared_header.runs().set_text("Shared header");

auto shared_footer = doc.assign_section_footer_paragraphs(1, 0);
shared_footer.runs().set_text("Shared footer");

auto shared_first_footer = doc.assign_section_footer_paragraphs(
    1, 0, featherdoc::section_reference_kind::first_page);
shared_first_footer.runs().set_text("Shared footer");

Use remove_section_header_reference(section_index, kind) and remove_section_footer_reference(section_index, kind) to detach a specific section-level reference without touching other kinds already attached to the same section.

doc.remove_section_header_reference(1);
doc.remove_section_footer_reference(
    1, featherdoc::section_reference_kind::first_page);

When a header/footer part is no longer referenced from document.xml, save() / save_as() automatically omit the orphaned part together with the matching document.xml.rels relationship and [Content_Types].xml override. Removing the last first-page or even-page reference also drops w:titlePg or w:evenAndOddHeaders when that flag is no longer needed.

Use remove_header_part(index) and remove_footer_part(index) when you want to drop one loaded header/footer part entirely. The matching section references are detached in memory, header_count() / footer_count() shrink immediately, and the orphaned ZIP entries are omitted on the next save.

doc.remove_header_part(1);
doc.remove_footer_part(1);

Use copy_section_header_references(source_section, target_section) and copy_section_footer_references(source_section, target_section) when one section should adopt another section's current header/footer reference layout. The target side is replaced for that reference family, so stale first / even references are removed automatically.

doc.copy_section_header_references(0, 1);
doc.copy_section_footer_references(0, 1);

Use replace_section_header_text(section_index, replacement, kind) and replace_section_footer_text(section_index, replacement, kind) when you want to rewrite a section-specific header/footer as plain paragraph text in one step. The replacement text is split on newlines, and the requested reference is created automatically when it is missing.

doc.replace_section_header_text(0, "Title line\nSubtitle line");
doc.replace_section_footer_text(
    0, "First page footer", featherdoc::section_reference_kind::first_page);

Use append_section(inherit_header_footer) to append a new final section at the end of the document. By default it inherits the previous final section's header/footer reference layout; passing false appends the new section without copying those references.

doc.append_section();
doc.append_section(false);

Use insert_section(section_index, inherit_header_footer) to insert a new section after an existing section. By default the inserted section inherits the referenced section's current header/footer reference layout; passing false creates the new section break without copying those references.

doc.insert_section(0);
doc.insert_section(1, false);

Use remove_section(section_index) to remove one section while preserving the document content around it. Removing a non-final section collapses its break so that content flows into the following section; removing the final section makes the previous section become the new final section.

doc.remove_section(1);

Use move_section(source_section_index, target_section_index) to reorder whole sections. The section content and its header/footer reference layout move together, and target_section_index is the final index of the moved section after reordering.

doc.move_section(2, 0);

Use create_empty() when you want to build a new .docx document from scratch without relying on an existing template archive.

featherdoc::Document doc("new-file.docx");
if (const auto error = doc.create_empty()) {
    std::cerr << error.message() << '\n';
    return 1;
}

doc.paragraphs().add_run("Hello FeatherDoc");
if (const auto error = doc.save()) {
    std::cerr << error.message() << '\n';
    return 1;
}

Use the default run font/language APIs when you want Chinese/CJK text to carry explicit w:rFonts and w:lang metadata instead of relying on Word's fallback heuristics.

featherdoc::Document doc("zh-demo.docx");
if (const auto error = doc.create_empty()) {
    std::cerr << error.message() << '\n';
    return 1;
}

if (!doc.set_default_run_font_family("Segoe UI") ||
    !doc.set_default_run_east_asia_font_family("Microsoft YaHei") ||
    !doc.set_default_run_language("en-US") ||
    !doc.set_default_run_east_asia_language("zh-CN")) {
    std::cerr << "failed to configure default run fonts/languages\n";
    return 1;
}

auto run = doc.paragraphs().add_run("你好,FeatherDoc。这里是一段中文/CJK 文本。");
if (!run.has_next()) {
    std::cerr << "failed to append Chinese/CJK paragraph\n";
    return 1;
}

if (const auto error = doc.save()) {
    std::cerr << error.message() << '\n';
    return 1;
}

The CLI now exposes the same docDefaults surface through inspect-default-run-properties, set-default-run-properties, and clear-default-run-properties when you need the same default font/language/RTL edits from scripts instead of C++.

When one paragraph needs its own override, call run.set_font_family(...), run.set_east_asia_font_family(...), run.set_language(...), and run.set_east_asia_language(...) on the returned Run. Use run.clear_primary_language(), run.clear_east_asia_font_family(), run.clear_east_asia_language(), and run.clear_bidi_language() when only one CJK/RTL override should be removed while preserving the rest of the run formatting. run.clear_language() still removes all w:lang attributes (w:val, w:eastAsia, and w:bidi) in one call. Use doc.inspect_paragraph_runs(paragraph_index) or doc.inspect_paragraph_run(paragraph_index, run_index) when you need a library-level summary of run style/font/language/RTL metadata without going through the CLI helpers. Use doc.inspect_paragraphs() or doc.inspect_paragraph(paragraph_index) when you need paragraph-level style/bidi/numbering/run-count metadata from the core library. Use doc.inspect_tables() or doc.inspect_table(table_index) when you need table-level style/width/grid/text metadata from the core library. Use featherdoc_cli inspect-tables when you need the same table inspection metadata from the command line. Use doc.inspect_table_cells(table_index) or doc.inspect_table_cell(table_index, row_index, cell_index) when you need cell-level width/span/layout/text metadata from the core library. Use featherdoc_cli inspect-table-cells when you need the same cell inspection metadata from the command line. Use featherdoc_cli inspect-table-rows when you need body-table row height/page-break/header-repeat/cell-text metadata from the command line. Use featherdoc_cli set-table-cell-text when you need to replace a specific table cell's plain text from the command line. Use featherdoc_cli set-table-cell-fill and clear-table-cell-fill when you need to add or remove a body-table cell background fill from the command line. Use featherdoc_cli set-table-cell-vertical-alignment and clear-table-cell-vertical-alignment when you need to edit body-table cell vertical alignment from the command line. Use featherdoc_cli set-table-cell-text-direction and clear-table-cell-text-direction when you need to edit body-table cell text direction from the command line. Use featherdoc_cli set-table-cell-width and clear-table-cell-width when you need to assign or remove an explicit body-table cell width from the command line. Use featherdoc_cli set-table-cell-margin and clear-table-cell-margin when you need to edit one edge of a body-table cell's internal margin from the command line. Use featherdoc_cli set-table-cell-border and clear-table-cell-border when you need to edit one edge of a body-table cell border from the command line. Use featherdoc_cli set-table-row-height and clear-table-row-height when you need to assign or remove an explicit body-table row height override from the command line. Use featherdoc_cli set-table-row-cant-split and clear-table-row-cant-split when you need to control whether a body-table row is allowed to split across pages from the command line. Use featherdoc_cli set-table-row-repeat-header and clear-table-row-repeat-header when you need to control whether a body-table row repeats as a header row from the command line. Use featherdoc_cli append-table-row, insert-table-row-before, insert-table-row-after, and remove-table-row when you need to append, insert, or remove body-table rows from the command line. When no explicit --cell-count is provided, appended rows default to the current table column count. Use featherdoc_cli merge-table-cells when you need to merge a specific body table cell to the right or downward from the command line. Use featherdoc_cli unmerge-table-cells when you need to split an existing horizontal or vertical body-table merge from the command line. Use featherdoc_cli merge-template-table-cells when you need to merge a specific table cell inside a template-part table selected through --part/--index/--section/--kind. Use featherdoc_cli unmerge-template-table-cells when you need to split an existing horizontal or vertical merge inside those template-part tables from the command line. Template-table inspection and mutation commands that accept a positional <table-index> also accept --bookmark <name> as an alternative selector. That bookmark can live inside the target table or immediately before it, which makes it the recommended way to anchor "the table on this page" in a DOCX workflow where rendered page numbers are not stable. For example, if page3_target_table is a bookmark placed inside the table you want to edit on a rendered page, you can drive the mutation without guessing a table index:

featherdoc_cli set-template-table-cell-text report.docx --bookmark page3_target_table 1 2 --text "Updated value" --output report-updated.docx --json
featherdoc_cli set-template-table-row-texts report-updated.docx --bookmark page3_target_table 3 --row "Item A" --cell "3" --cell "99.00" --row "Item B" --cell "1" --cell "18.00" --output report-updated.docx --json
featherdoc_cli set-template-table-cell-block-texts report-updated.docx --bookmark page3_target_table 3 1 --row "North" --cell "120" --row "South" --cell "98" --output report-updated.docx --json
featherdoc_cli append-template-table-row report-updated.docx --bookmark page3_target_table --output report-updated.docx --json

The C++ API exposes the same workflow on TemplatePart: call find_table_by_bookmark(...) on body_template(), header_template(), footer_template(), section_header_template(), or section_footer_template() and mutate the returned Table handle directly. That Table handle now also supports direct indexed helpers such as find_row(...), find_cell(...), set_cell_text(...), and set_row_texts(...), plus batch helpers such as set_rows_texts(...) and set_cell_block_texts(...), so page-local table edits no longer require manual next() loops in user code. Use doc.inspect_sections() or doc.inspect_section(section_index) when you need a section/header/footer summary of both the explicit default/first/even references stored on each section and the resolved linked-to-previous fallback chain, including the underlying word/headerN.xml / word/footerN.xml entry names, the source section index that currently supplies each slot, and the document-wide w:evenAndOddHeaders setting from word/settings.xml when it can be read. The same inspection summaries are also available on TemplatePart handles returned by body_template(), header_template(), footer_template(), section_header_template(), and section_footer_template(). For a runnable end-to-end version, build featherdoc_sample_chinese from samples/sample_chinese.cpp with -DBUILD_SAMPLES=ON.

Performance Notes

  • open() now keeps XML buffer ownership on the FeatherDoc side before parsing, which avoids cross-library allocator mismatches in shared-library builds.
  • save() now streams document.xml directly into the output archive instead of materializing one large intermediate string.
  • save() also copies non-XML ZIP entries chunk-by-chunk, which avoids loading each entry into heap memory before writing it back.

Current Limitations

  • Password-protected or encrypted .docx files are not supported yet.
  • Section-specific header/footer references can now be created and rebound through ensure_section_header_paragraphs() / ensure_section_footer_paragraphs() and assign_section_header_paragraphs() / assign_section_footer_paragraphs(), and removed through remove_section_header_reference() / remove_section_footer_reference(). Whole parts can also be dropped through remove_header_part() / remove_footer_part(), and section reference layouts can be copied through copy_section_header_references() / copy_section_footer_references(). New sections can now be appended or inserted after an existing section through append_section() / insert_section(), removed through remove_section(), and reordered through move_section(). Header/footer part indexes can now also be reordered through move_header_part() / move_footer_part().
  • Word equations (OMML) are not surfaced through a typed equation API.
  • Tables can now be appended, extended structurally, given explicit cell, column, and table widths, merged horizontally and vertically, assigned table/cell borders, switched between fixed and autofit layout, aligned/indented within the page, pointed at existing table style ids, given basic table-level default cell margins and cell shading/margins, assigned row heights, controlled for page splitting, assigned cell vertical alignment, marked to repeat header rows, and retuned through tblLook style-routing flags, but there is still no high-level API for custom table style definitions or floating table positioning.
  • Paragraphs can now be attached to managed bullet and decimal lists and can restart managed list sequences. Custom numbering definitions and paragraph-style numbering are now supported through ensure_numbering_definition(...) and set_paragraph_style_numbering(...), and multi-style shared outline linking is now available through ensure_style_linked_numbering(...), but there is still no richer import/export or override-management layer for existing numbering catalogs.
  • Paragraph and run style references can now be attached and cleared, and a minimal word/styles.xml is created automatically when needed. Style catalog inspection and minimal paragraph/character/table style definition editing are now available through list_styles(), find_style(), find_style_usage(), resolve_style_properties(), and the ensure_*_style(...) helpers. Effective inherited font/language/RTL/paragraph-bidi inspection is now available, and materialize_style_run_properties(...), rebase_paragraph_style_based_on(...), and rebase_character_style_based_on(...) can now freeze supported inherited properties onto the child style before a basedOn rewrite. Broader style-linked numbering and higher-level style refactoring workflows are still missing.
  • Bookmark-based template filling now works across body, header, and footer parts through fill_bookmarks(...), the standalone replacement helpers, and TemplatePart handles returned by body_template(), header_template(), footer_template(), section_header_template(), and section_footer_template(). Conditional block visibility is now supported through set_bookmark_block_visibility(...) and apply_bookmark_block_visibility(...), and validate_template(...) now covers slot declarations, missing required slots, duplicate names, malformed placeholders, unexpected bookmarks, kind mismatches, and occurrence constraints. Document-level multi-part schema validation is now available through validate_template_schema(...) plus featherdoc_cli validate-template-schema, and reusable JSON schema files can now be fed through --schema-file, but there is still no richer schema mutation layer or external schema-management toolchain.
  • Images can now be appended as inline body drawings, enumerated through inline_images() or the broader drawing_images(), extracted through extract_inline_image(...) / extract_drawing_image(...), removed through remove_inline_image(...) / remove_drawing_image(...), and replaced through replace_inline_image(...) / replace_drawing_image(...). Floating body image creation is now supported through append_floating_image(...), and bookmark-based floating image replacement is available through replace_bookmark_with_floating_image(...) across body, header, and footer TemplatePart handles. Rectangular wrapping, crop values, overlap control, and anchor z-order are now exposed through floating_image_options, but more advanced drawing behaviors are still not surfaced as high-level APIs.

Source Layout

The core implementation is now split into focused translation units instead of living in a single large .cpp file:

  • src/document.cpp: Document open/save flow, archive handling, and error reporting
  • src/document_image.cpp: inline body image insertion, enumeration, extraction, replacement, media part allocation, and drawing relationship updates
  • src/document_numbering.cpp: managed paragraph list numbering, numbering part attachment, and numbering definition generation
  • src/document_styles.cpp: paragraph/run style references and word/styles.xml attachment/persistence
  • src/document_template.cpp: bookmark-based template filling and batch replacement APIs
  • src/paragraph.cpp: paragraph traversal, run creation, paragraph insertion, and paragraph-property cloning
  • src/image_helpers.cpp / src/image_helpers.hpp: image binary loading plus file format and size detection helpers
  • src/run.cpp: run traversal, text/property edits, and run insertion/removal
  • src/table.cpp: table creation plus row/cell traversal and editing helpers
  • src/xml_helpers.cpp / src/xml_helpers.hpp: internal XML helper utilities shared by the modules
  • src/constants.cpp: exported constants and error-category plumbing
  • cli/featherdoc_cli.cpp: scriptable inspection and editing utility for sections, styles, numbering, page setup, bookmarks, images, and template parts

This layout keeps archive I/O, XML navigation, and public API objects easier to reason about and extend independently.

Bundled Dependencies

  • pugixml 1.15
  • kuba--/zip 0.3.8
  • doctest 2.5.1

Documentation

  • Simplified Chinese README: README.zh-CN.md
  • Changelog: CHANGELOG.md
  • Sphinx docs entry: docs/index.rst
  • Project identity guide: docs/project_identity_zh.rst
  • Current direction guide (Chinese): docs/current_direction_zh.rst
  • Release policy guide: docs/release_policy_zh.rst
  • Chinese license guide: docs/licensing_zh.rst
  • Repository legal notes: LEGAL.md
  • Distribution notice summary: NOTICE

Project Direction

FeatherDoc should be treated as its own actively-shaped fork rather than a passive mirror of the historical upstream project.

  • The product goal is a .docx engine for formal document workflows, covering document processing, structured editing, generation, and verification.
  • Modern C++ and clearer API semantics take priority over preserving obsolete compatibility patterns.
  • MSVC buildability is a real support target, not a best-effort afterthought.
  • Error diagnostics, save/open behavior, and core-path performance are first class concerns.
  • Project positioning, licensing, documentation, and repository metadata follow the current FeatherDoc direction.

If you need the current planning document that explains what to build next, see docs/current_direction_zh.rst.

Sponsor

If this project helps your work, you can support ongoing maintenance via the following support QR codes.

Alipay QR Code WeChat Appreciation QR Code

Left: Alipay. Right: WeChat Appreciation.

License

This fork should be described as source-available rather than open source for its fork-specific modifications.

  • Fork-specific FeatherDoc modifications are distributed under the non-commercial source-available terms in LICENSE.
  • Upstream DuckX-derived portions still keep the original MIT text preserved in LICENSE.upstream-mit.
  • Bundled third-party dependencies keep their own original licenses.
  • Practical Chinese reading guide: docs/licensing_zh.rst
  • Short repository legal guide: LEGAL.md
  • Distribution notice file: NOTICE

About

Modern C++ .docx library with MSVC support, Word visual validation, and existing-document editing APIs

Resources

License

Unknown, MIT licenses found

Licenses found

Unknown
LICENSE
MIT
LICENSE.upstream-mit

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • C++ 58.3%
  • PowerShell 40.8%
  • Other 0.9%