Время прочтения: 5 мин.

В этом году я учувствовал в конкурсе по реализации сервиса, который должен проверять формат оформления документов и вносить изменения в режиме правки. Существующие библиотеки либо не решали эту задачу вовсе, либо оказались платными. Было принято решение погрузиться в формат документа MS Word (Office Open XML) и написать свою библиотеку на .net Framework.

Как устроен DOCX

.docx файл – это zip архив. Он содержит в себе разметку и содержание документа в виде xml и других файлов. Его можно распаковать с помощью архиватора:

Document.xml – файл, который содержит в себе разметку параграфов и таблиц документ, за исключением колонтитулов и сносок. Эти блоки вынесены в отдельные файлы.

Содержимое файла:

<w:document ... >
    <w:body><!-- в body последовательно, будут перечислены абзацы и таблицы -->
        <!-- абзац. может содержать несколько Run'ов. У каждого Run может быть свой стиль оформления -->
        <w:p w:rsidR="00B93B17" w:rsidRPr="009D49F6" w:rsidRDefault="00162675" w:rsidP="00C83C69">
            <w:pPr> <!-- свойства абзаца -->
                <w:rPr> <!-- свойства Run (w:r) -->
                    <w:lang w:val="en-US"/>
                </w:rPr>
            </w:pPr>
            <w:r w:rsidRPr="00C83C69"> <!--Run. может содержать текст, картинки и тп -->
                <w:rPr><!-- свойства Run. Здесь хранится инфо о формате текста. Шрифт, размре, цвет, ссылка на стиль и тп -->
                    <w:rStyle w:val="ad"/>
                    <w:rFonts w:eastAsiaTheme="majorEastAsia"/>
                    <w:i w:val="0"/>
                </w:rPr>
                <w:t>1</w:t> <!-- текст Run'а-->
            </w:r>
            <w:r>
                ...
            </w:r>
            
        </w:p>
        <w:sectPr w:rsidR="00B93B17" w:rsidRPr="009D49F6" w:rsidSect="00E142D1">
            <w:headerReference w:type="even" r:id="rId8"/>  <!--ссылка на файл заголовка по ID можно вычислить путь к файлу в _rels\document.xml.rels -->             
            <!--и другие ссылки -->
            <w:pgSz w:w="11906" w:h="16838"/>
            <w:pgMar w:top="1134" w:right="1134" w:bottom="1134" w:left="1134" w:header="709" w:footer="709" w:gutter="0"/>
            <w:cols w:space="708"/>
            <w:docGrid w:linePitch="360"/>
        </w:sectPr>
    </w:body>
</w:document>

Мы можем изменить document.xml и\или другие файлы, запаковать все в zip архив, переименовать его в .docx и открыть с помощью MS Word. Если правила разметки не нарушены MS Word сможет его отобразить. На этом принципе основана работа библиотеки TDV.Docx.

Библиотека не охватывает все возможности изменения документа, а лишь основную часть. Добавление параграфов, таблиц, изображений, колонтитулы и рецензирование.

Правки документа

Первым делом нужно подключить библиотеку:

using TDV.Docx;

Открытие и сохранение документа:

using (FileStream fs = new FileStream("1.docx", FileMode.Open))
{
    DocxDocument doc = new DocxDocument(fs);
/* change code */
    doc.document.Apply(); // Метод Apply() применяет изменения к файлу (в данном случае к document.xml)
    //Если вы изменяете другие файлы, например верхний колонтитул, для них так же нужно вызывать метод Apply()
    //cохранинение файла
    using (FileStream sw = new FileStream("1_fixed.docx", FileMode.OpenOrCreate))
    {
        byte[] b = doc.ToBytes();
        sw.Write(doc.ToBytes(), 0, b.Length);
    }
}

Далее предполагается, что doc — экземпляр DocxDocument.

Тело документа содержит в себе последовательность параграфов и таблиц.
Все эти классы унаследованы от базового Node. Перебирая ноды и ориентируясь на их содержимое можно эффективно осуществлять навигацию по документу.

foreach(Node node in doc.document.body.childNodes)
{
    if (node is Table) 
    {
        Table tbl = (Table)node;
        Tc cell = tbl.GetCell(0, 0);
        foreach (Paragraph p in cell.Paragraphs)
        {
            if (p.Text == "")
                p.CorrectDel("Дядя Вася"); //Удаление в режиме правки
        }
    }
    if (node is Paragraph)
    {
        Paragraph p = (Paragraph)node;
        p.Text = "это параграф";
    }
}

Каждый параграф содержит параметры стиля. Если параграф содержит ссылку на стиль, сначала применяются параметры стиля, затем параметры параграфа.
Например, если в стиле указано выравнивание по центру, а в свойствах параграфа справа в итоге будет выравнивание по правому краю.
Аналогично устроены все параметры.

Свойства параграфа содержат в себе свойства Run (w:rPr). Каждый Run так же содержит раздел свойств. Свойства Run более приоритетны чем свойства родительского параграфа.

Получить/установить параметры параграфа несложно:

Paragraph p = (Paragraph)node;
p.pPr.HorizontalAlign = HORIZONTAL_ALIGN.BOTH;
p.pPr.ind.firstLine = 1.25f;               //отступ первой строки
p.pPr.pBdr.Bottom = new Border(LINE_TYPE.SINGLE, 4);//Нижняя граница линия, толщина 4
p.pPr.pBdr.Between = new Border();         //Граница между параграфами - нет
p.pPr.rPr.IsBold=false;                    // Обращение к дефолтным свойствам Run
p.pPr.spacing.after = 0;                   // отсутп после абзаца
p.pPr.spacing.before = 0;                  // отсутп перед абзацем
p.pPr.spacing.line = 1;                    // Межстрочный интервал     

Класс PStyle содержит в себе все свойства параграфа.

PStyle pStyle = new PStyle(HORIZONTAL_ALIGN.LEFT, new Border(), new Border(), new Border(),
    new Border(), new Border(), new Border(), 0, 0, 0, 0, 0, 0, 0);
p.pPr.SetStyle(pStyle); //Применить стиль pStyle к параграфу p

Аналогичным образом устроены стили Run.

Paragraph p = (Paragraph)node;
foreach (R r in p.rNodes)
{
    RProp runProp = r.rPr;
    runProp.border.border = new Border();   //Нет границы Run
    runProp.Color = "#ffffff";              //Белый цвет
    runProp.Highlight = "#000000";          //черная заливка
    runProp.IsBold = false;                 //не жирный
    runProp.IsItalic = true;                //курсив
    runProp.IsStrike = false;               //не зачеркнутый
    runProp.Underline = LINE_TYPE.DOTTED;   //подчеркнутый. линия из точек
    runProp.Font = "Times New Roman";       //шрифт
    runProp.FontSize = 10.5f;               //размер шрифта
}
RStyle rStyle = new RStyle(true, "Times New Roman", 22, false, false, LINE_TYPE.NONE, "", "",new Border());
Paragraph p = (Paragraph)node;
foreach (R r in p.rNodes)
    r.rPr.SetStyle(rStyle);  

Сравнение в режиме правки (рецензирование)

Для редактирования документа в режиме правки придуманы отдельные методы. Их название начинается с «Correct..» или «Compare..».

Исходный документ выглядит так:

Изменение текста параграфа:

Paragraph p = (Paragraph)node;
p.CorrectSetText("новый текст", rStyle, "Имя автора");

Результат:

Изменение стиля параграфа:

PStyle pStyle = new PStyle(HORIZONTAL_ALIGN.LEFT, new Border(), new Border(), new Border(),
                    new Border(LINE_TYPE.SINGLE,4,0,"#f5f111"), new Border(), new Border(), 0, 0, 1, 2, 0, 0, 0);
Paragraph p = (Paragraph)node;
p.ComparePStyle(pStyle, "Имя автора");

Результат:

Описывать весь функционал здесь я не буду, в репозитории есть инструкция с примерами. Библиотека так же позволяет осуществлять рецензирование стилей Run, таблиц, колонтитулов, сносок, вставка и удаление параграфов, работу с изображениями. Подробную информацию можно найти в репозитории на GitHub.