Соберите договор или акт в Word по ГОСТу
Создание, редактирование, объединение и ремонт документов Word (.docx) через C# OpenXML SDK. Конвейеры: A — создание (договор, акт, приказ, доверенность, КП, протокол, резюме, письмо, служебная записка, карточка предприятия); B — правка существующего файла (заменить фразу, заполнить реквизиты, исправить, убрать, поправить); C — переформатирование/шаблон; D — объединить, склеить, подшить акт к заключению, добавить приложение; E — починить повреждённый .docx.
Как агент работает
Задача сначала разводится по конвейерам. A — создание с нуля: договор, акт, приказ, доверенность, коммерческое предложение, протокол, служебная записка, письмо, карточка предприятия. B — правка готового файла: заменить фразу, дозаполнить реквизиты, убрать лишнее. C — переформатирование под шаблон наложением styles.xml или подстановкой в заглушки. D — объединение и подшивка приложений. E — ремонт повреждённого файла, после которого с ним работают остальные конвейеры.
Оформление идёт по ГОСТ Р 7.0.97-2016: поля 1134 твипа сверху и снизу, 1701 слева и 567 справа, Times New Roman 14 пт, что в разметке записывается как sz равный 28, межстрочный интервал 1,0 при Line 240, красная строка 1,25 см — это 709 твипов, отбивка между абзацами 6 пт при After 120. Единицы пересчитываются честно: 1 см — 567 твипов, дюйм — 1440, а в EMU дюйм равен 914400.
Документ собирается с соблюдением порядка элементов OpenXML, из-за нарушения которого Word и отказывается открывать файл. В абзаце свойства идут перед прогонами текста, в таблице — свойства, затем сетка колонок, затем строки, в ячейке минимум один абзац, а замыкающие свойства секции стоят последним потомком тела. Прогон текста живёт только внутри абзаца: висячий run и абзац внутри абзаца дают ровно тот файл, который открывает LibreOffice и не открывает Word.
Таблицы строятся так, чтобы сетка сходилась с реальным числом колонок, а полноширинная строка — баннер, итог или приписка — занимала весь ряд объединением ячеек на нужное число колонок. Разделы нумеруются сквозным счётчиком, а заголовки для оглавления ставятся стилевые. Деньги форматируются культуронезависимо, иначе английская локаль песочницы напечатает «3,233,10» вместо «3233,10».
Результат не отдаётся вслепую. Выходной файл всегда называется иначе, чем входной, чтобы исходник остался цел; после сборки пакет чистится от лишнего BOM, структура проверяется валидатором, а предпросмотр в PDF открывается и просматривается до выдачи. Документ остаётся чёрно-белым, русский текст — кириллицей, и готовые файлы отдаются пользователю явно, а не остаются в рабочей папке.
word-document (OpenXML SDK)
Маршрутизация
Output-файл называется иначе чем input (document.docx → document_v2.docx, document_fixed.docx).
Конвейер B: правка
edits.json пишется edit_file(op='overwrite') — путь $WORK_ROOT/edits.json, содержимое целиком в replace:
Чистый текст для подбора фразы:
Структурные правки таблиц/секций — адресной правкой поверх WordprocessingDocument.Open(path, true). При комбинации с текстом: сначала edit_document.csx, потом структурная правка на выходе.
Документы с <w:ins>/<w:del> — сначала принять исправления в Word.
Конвейер E: ремонт
Диагностика:
Ремонт:
Дальше B/C/D работают с *_fixed.docx.
Конвейер C: шаблон
- C-1 наложение: source → output, заменить
styles.xmlиз шаблона, применить черезpStyle. - C-2 база-замена: шаблон → output, заменить контент-заглушки текстом из source.
Конвейер A: создание
task.csx пишется и переписывается ТОЛЬКО через edit_file(op='overwrite'): путь $WORK_ROOT/task.csx, всё содержимое файла целиком в replace. Файл маленький, полная перезапись дешевле любой точечной правки и всегда даёт байт-в-байт предсказуемый результат.
Чего не делать:
- НИКОГДА не писать
.csxкомандой шелла (cat, heredoc,echo,>/>>): оборванный разделитель кладёт в файл собственные строки и даёт каскад CS1003/CS1026/CS1513 на ровном месте. - НИКОГДА не дописывать в существующий
task.csx: второй прогон удвоитusingи объявления (CS0102/CS0229), а#r, уехавший из первых строк файла, даст CS1529 и каскад CS0246 на все типы пакета. - НИКОГДА не править
.csxчерезedit_file(op='update')— search-блок почти наверняка разойдётся с файлом по байтам (отступы, переносы) →No match for search block, similarity 0.3–0.8 и 2–3 сожжённые итерации. Именно поэтомуoverwrite, а неupdate.
Первые три строки файла — дословно, без них не резолвится пакет:
Дальше — тело (полный скелет файла):
Запуск:
Правила .csx
- Жизненный цикл
WordprocessingDocument— внутри блока{ }.using var doc = ...всегда в блоке. - Порядок директив:
#r→#load→using→ код. EnsureWordReadyParts(doc)— последняя строка внутри блока.StripPackageBom(outputPath)— ОБЯЗАТЕЛЬНО первой строкой ПОСЛЕ закрытия блока{ }: System.IO.Packaging пишет UTF-8 BOM в[Content_Types].xml/.rels/.psmdcp, хелпер его срезает.- Таблицы строй через
CreateTable(...)— он сам кладётtblGrid. Полноширинная строка-баннер/итог/приписка —SpanCell(text, span: N), гдеN= число колонок грида. - Создавай выходную директорию:
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!); - Части пакета — через
mainPart.AddNewPart<T>(), затем наполнение. - Leaf-элементы — object-initializer:
new Justification { Val = JustificationValues.Center },new Color { Val = "auto" },new Bold(). Bodyсоздавай вручную:new Document(new Body()).- Готовый
Paragraphклади как есть —cell.Append(MakePara(...)),body.Append(MakePara(...)). НЕ оборачивай его в ещё один абзац:new Paragraph(MakePara(...))иnew TableCell(new Paragraph(MakePara(...)))дают<w:p>внутри<w:p>, и Word отказывается открывать файл, хотя LibreOffice и превью его открывают. В ячейку —MakeCell(...)илиnew TableCell(tcPr, MakePara(...))(один абзац). Run только внутри абзаца:body.Append(MakePara(text)), неbody.Append(run). Нужен курсив/кегль/жирный —MakePara(text, align, bold, italic, sizePt), а НЕ raw Run. StripPackageBom лечит это на выходе, но не плоди. MakePara,LargeHeading,Pullquote,Heading1/Heading2— свободные функции, возвращают готовыйParagraph; клади прямо в body:body.Append(Heading1("ПРЕДМЕТ")),body.Append(MakePara(...)). НЕ передавайParagraphобратно вMakePara(...)— он принимаетstring,MakePara(sec.Next(...))не скомпилируется.SectionCounter— ЕДИНСТВЕННЫЙ helper с состоянием (счётчик разделов). ОБЯЗАТЕЛЬНО объявиvar sec = new SectionCounter();ДО первогоsec.Next("УСЛУГИ"), иначеCS0103: имя 'sec' не существует в текущем контексте.- Стилевой заголовок — свободные функции
Heading1("…")/Heading2("…"), а НЕ членыSectionCounter:SectionCounter.Heading1(...)дастCS0117: SectionCounter не содержит определения Heading1.Heading1/Heading2применяют ГОСТ-стили (нужныGostHeading1Style()/GostHeading2Style()вStyles);sec.Next(...)— нумерованный жирный абзац стиля Normal без outlineLvl. - Выравнивание — только энум
JustificationValues(.Left/.Center/.Right/.Both).JustAlignmentне существует. Append(...)возвращает void: не кастуй его и не ссылайся на его результат (row.Append(table.Append(row) as TableRow)— мусор, не компилируется). Собирай по шагам:var row = new TableRow(); row.Append(MakeCell(...)); table.Append(row);— один.Append()на строку.- Перегрузки вместо default-параметров.
- Жирный/выровненный абзац:
align-энум идёт ПЕРЕД флагомbool bold.MakePara(text, JustificationValues.Left, true)— жирный с явным выравниванием;MakePara(text, true)— только жирный (выравнивание Left). УMakeCell/ShadedCell/SpanCellхвост —…, widthDxa, bool bold, JustificationValues align(MakeCell(text, "0", true, JustificationValues.Right), width строкой, "0"=auto). Cell-хелперы вызывай полной пятёркой; частичные позиционные вызовы дают невнятную ошибкуCS1503: cannot convert from 'bool' to 'JustificationValues'— если её видишь, проверь именно порядок align/bold в этой строке. - Строки ≤ 120 символов. Длинный текст в переменную.
- Один
.Append()на строку.
Готовый пример: таблица из данных + столбец +30% + строка ИТОГО
Сквозной шаблон для КП/прайса (исходная таблица → новый вычисляемый столбец → итог). Компилируется как есть — меняй только headers/widths/items:
Строка ИТОГО: SpanCell(text, span, "0", bold, align) на первые span колонок + обычные MakeCell на остаток (span + число ячеек = число колонок грида). Жирная подпись-итог абзацем — MakePara(text, JustificationValues.Left, true). Деньги форматируй культуронезависимо через Money(...) (F2 + InvariantCulture + замена точки на запятую): N2+Replace(".", ",") под en-локалью сандбокса даёт «3,233,10» вместо «3233,10».
Типографика ГОСТ Р 7.0.97-2016
- Поля: top=1134, bottom=1134, left=1701, right=567 (твипы)
- Шрифт: Times New Roman 14пт (
sz="28"), атрибутыAscii/HighAnsi/EastAsia/ComplexScript - Цвет:
new Color { Val = "auto" }(или"000000"); границы таблиц"000000" - Межстрочный: 1.0 (
Line="240"). 1.5 — по явной просьбе - Красная строка: 1.25 см (
FirstLine="709") — вGostNormalStyle, вMakeCellобнулена - Между абзацами: 6пт (
After="120") +ContextualSpacing - Заголовки: Heading1
Before=180/After=60, Heading2Before=120/After=60 - Переносы:
EnableAutoHyphenation(mainPart)приJustificationValues.Both - Нумерация:
AddGostPageNumberHeader+GostPageSetupWithNumbers; убрать →GostPageSetup()
Документ чёрно-белый. Русский текст кириллицей.
Пары шрифтов:
| Назначение | Вариант 1 | Вариант 2 |
|---|---|---|
| Основной текст | Times New Roman | PT Serif |
| Заголовки | Arial | PT Sans |
| Моноширинный | Courier New | JetBrains Mono |
Единицы
w:sz= пункты × 2 (14пт →sz="28")- Твипы (1/20 пт): 1 см = 567, 1 дюйм = 1440.
Dxaв коде. LineприLineRule=Auto: 240=1.0, 360=1.5, 480=2.0SpacingBetweenLines.After: 120=6пт, 240=12пт- EMU: 1 дюйм = 914400, 1 см = 360000
Enum-ы
TableWidthUnitValues: Dxa, Pct (×50, 5000=100%), Auto (Width="0"), Nil
JustificationValues: Left, Center, Right, Both, Distribute, Start, End
BorderValues: Single, Double, Triple, Thick, Dotted, Dashed, DashDotStroked, DotDash, DotDotDash, None, Nil
BreakValues: Page, Column, TextWrapping
PageOrientationValues: Portrait, Landscape
SectionMarkValues: NextPage, NextColumn, Continuous, EvenPage, OddPage
MergedCellValues: Restart, Continue
StyleValues: Paragraph, Character, Table, Numbering
TableVerticalAlignmentValues: Top, Center, Bottom
TableRowAlignmentValues: Left, Center, Right
LineSpacingRuleValues: Auto, Exact, AtLeast
HeaderFooterValues: Default, First, Even
UnderlineValues: Single, Double, Thick, Dotted, Dash, Wave, None
HighlightColorValues: None, Black, Blue, Cyan, Green, Magenta, Red, Yellow, White, DarkBlue, DarkCyan, DarkGreen, DarkMagenta, DarkRed, DarkYellow, DarkGray, LightGray
VerticalPositionValues: Baseline, Superscript, Subscript
Порядок XML
| Родитель | Дети |
|---|---|
Paragraph | ParagraphProperties → Runs |
Run | RunProperties → Text/Break/TabChar |
Table | TableProperties → TableGrid → Rows |
TableRow | TableRowProperties → Cells |
TableCell | TableCellProperties → ≥1 Paragraph |
Body | блоки → SectionProperties последним |
TableProperties (CT_TblPrBase):
tblStyle → tblpPr → tblOverlap → bidiVisual → tblStyleRowBandSize → tblStyleColBandSize → tblW → jc → tblCellSpacing → tblInd → tblBorders → shd → tblLayout → tblCellMar → tblLook
ParagraphProperties (CT_PPrBase):
pStyle → keepNext → keepLines → pageBreakBefore → framePr → widowControl → numPr → suppressLineNumbers → pBdr → shd → tabs → spacing → ind → contextualSpacing → mirrorIndents → suppressOverlap → jc → textDirection → outlineLvl
RunProperties (CT_RPr):
rStyle → rFonts → b → bCs → i → iCs → caps → smallCaps → strike → dstrike → outline → shadow → emboss → imprint → noProof → snapToGrid → vanish → webHidden → color → spacing → w → kern → position → sz → szCs → highlight → u → effect → bdr → shd → fitText → vertAlign → rtl → cs → em → lang
Пересобирай *Properties целиком или вставляй через InsertBefore/InsertAfter.
Text — сиблинг RunProperties. Run ВСЕГДА внутри абзаца. Курсив/кегль/жирный —
через MakePara(text, align, bold, italic, sizePt), НЕ raw body.Append(new Run(...))
(висячий run + порядок детей rPr — главная причина «файл повреждён»; авто-лечение на
выходе есть, но не плоди):
Сложная вёрстка (газеты, буклеты, постеры)
Колонки
EndColumns() обязателен.
Зебра
Ручной new Table() — только когда нужен per-cell контроль (зебра, merge). Тогда:
TableProperties → TableGrid (число GridColumn = max ширине строки) → строки.
Полноширинная строка через SpanCell(text, span: <число колонок>). EnsureWordReadyParts
(в edit/merge/fill — NormalizeTables) достраивает грид и урезает переспан, но это
страховка, а не замена правильной сборки. Контроль — validate_document.csx.
Страницы
Разрыв страницы: new Run(new Break { Type = BreakValues.Page }).
Промежуточный sectPr — внутри ParagraphProperties:
Альбомная: new PageSize { Width = 16838U, Height = 11906U, Orient = PageOrientationValues.Landscape }.
Финальный sectPr — последний потомок Body.
Деловые документы
| Тип | Структура |
|---|---|
| Договор | Шапка с реквизитами, нумерованные разделы, подписи |
| Акт | Дата/номер, описание, таблица стоимости, подписи |
| Приказ | "ПРИКАЗ", номер, дата, "ПРИКАЗЫВАЮ:", пункты |
| Доверенность | Доверитель, представитель, полномочия, срок, подпись, печать |
| Счёт-фактура | Табличная форма, реквизиты сторон |
| Протокол | Заседание, повестка, СЛУШАЛИ/РЕШИЛИ, голосование |
| Служебная записка | Кому, от кого, заголовок, текст, подпись |
| Коммерческое предложение | Логотип, услуги, цены, контакты |
| Письмо | Бланк, исх. номер, адресат, текст, подпись |
| Карточка предприятия | Реквизиты: наименование, ИНН, КПП, адрес, банк, контакты |
Проверка результата
Структурная:
Визуальная:
Чек-лист
- Output ≠ input по имени файла.
- Правка существующего →
edit_document.csx. - Объединение →
merge_document.csx. - Сломанный input →
repair_document.csxпервым. EnsureWordReadyParts(doc)— последняя строка в using-блоке;StripPackageBom(outputPath)— сразу после закрытия блока.EnableAutoHyphenation(mainPart)при justify.Line="240"если не просили 1.5.After="120"или меньше.- Нумерация:
AddGostPageNumberHeader+GostPageSetupWithNumbers; без неё →GostPageSetup(). - Нумерация разделов —
SectionCounter(объявиvar sec = new SectionCounter();); стилевой заголовок для оглавления —Heading1/Heading2. - Чекбоксы —
CheckBox(bool). - Таблицы —
CreateTableили ручнойTableсtblGrid(колонок = max ширине строки); полноширинная строка —SpanCell(span = число колонок). - PDF-превью открыто и проверено.
Сохранение
Файл в $OUTPUT_ROOT/. После сохранения → present_files.
Попробуйте этот навык
Зарегистрируйтесь и используйте навык «Создание и редактирование Word-документов (OpenXML)» бесплатно.