воскресенье, 1 сентября 2013 г.

Добавление изображения в документ Word через OpenXML

Разве это не очевидно? Чем мы всё чаще сталкиваемся с задачами, связанными с автоматизацией создания и редактирования "ms officных" документов, тем нам всё чаще приходит на ум название: Open XML SDK 2.0 for Microsoft Office. Сегодня речь пойдет об автоматизации вставки изображения в определенную область в документе формата MS Word (.docx).

Пример разработки приложения для вставки изображения в документ MS Word 

Определение области в документе для вставки изображения

Для того чтобы задать определенное место в документе, в которое будет осуществлена вставка изображения мы воспользуемся функционалом из вкладки "Разработчик". 

Если у вас данная вкладка не отображается, то включить ее можно в "Параметры Word" -> "Настройка ленты".

Далее в самом документе ставим курсор в необходимое место (в котором необходимо разместить рисунок), нажимаем на панели "Разработчик" кнопку: Элемент управления содержимым 'рисунок', как показано на примере ниже.

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

Нас особо интересует возможность редактирования свойств данной области - "Название" и "Тег". Для этого выделите область и нажмите "Свойства" на панели "Разработчик". В поле "Название" мы укажем произвольное имя данной области, например - "Здесь будет картинка!". В поле "Тег" впишем особую метку для данной области, которую будем использовать далее, например - "рисунок".

Нажимаем "ОК" на панели свойств. Сохраняем документ. В итоге документ принимает вид как на рисунке ниже. 

Подготовка закончена!

Начало работы с Open XML SDK 2.0 for Microsoft Office

Для старта нам необходимо скачать и установить инсталляционный пакет Open XML SDK 2.0 for Microsoft Office из Download Center. После инсталляции пакета в папке установки Open XML SDK по пути Open XML SDK\V2.0\lib можно найти библиотеку DocumentFormat.OpenXml.dll, которой мы и воспользуемся.

Подготовка проекта в Visual Studio 2010

В рамках данной статьи я продемонстрирую, на примере создания Windows Forms приложения на языке C#. Для начала необходимо создать в Visual Studio 2010 новый проект типа "Windows Forms Application". Я назвал проект "WordInsertPicture".

Далее в проекте нужно добавить референс на библиотеку DocumentFormat.OpenXml.dll, которую для удобства можно скопировать в папку с проектом.

WordprocessingDocument

Для открытия документа Word из потока используем класс WordprocessingDocument, следующим образом:
using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(myStream, false))
{
  foreach (var item in wordDoc.MainDocumentPart.Document.Body)
  {
      var oo = item.Descendants<SdtProperties>();
      foreach (var f1 in oo)
      {
          _contName = FindPictureContainer(wordDoc, f1, ref _sdtPropId);
      }
  }
}
В цикле foreach перебираем все элементы в теле документа Body. Далее для каждого элемента проверяем наличие тега <w:sdtPr>  и обозначающего: Structured Document Tag Properties. В WordprocessingDocument этот тег десериализуется в класс SdtProperties.  Этот элемент определяет набор свойств, которые должны применяться к родительскому тегу документа. Родительским тегом в нашем случае является тег <w:sdt>, в котором и содержится искомая область для вставки рисунка <w:drawing>.

 
  
  
  
  
  
 
 
  
   
    
   
   
    
     
     
    
    
     
      
      
      
      
       
      
      
       
        
         
          
          
           
          
         
         
          
           
            
             
            
           
          
          
          
           
          
         
         
          
           
           
          
          
           
          
          
          
           
          
         
        
       
      
     
    
   
  
 



Поиск области для вставки

Для поиска нужной области для вставки изображения воспользуемся свойством "Тег", значение которого мы определили как "рисунок" в предыдущем пункте. В OpenXML данному свойству соответствует тег <w:tag>, который десериализуется в класс Tag.
Также нас интересуют теги:

  • <w:alias>  он же SdtAlias, название области, которое мы задали в самом документе
  • <w:id> он же SdtId, ID области, является уникальным в рамках одного документа

В листинге, приведенном ниже, я описал метод, в котором осуществляется проверка соответствия тега в области значению - "рисунок", после чего возвращаются значения alias и id.
private string FindPictureContainer(WordprocessingDocument wdDoc, OpenXmlElement uy, ref string SdtId)
{
    SdtAlias alias = uy.Elements<SdtAlias>().FirstOrDefault();
    SdtId sdtId = uy.Elements<SdtId>().FirstOrDefault();
    Tag tag = uy.Elements<Tag>().FirstOrDefault();

    string _tag = "";
    string _sdtId = "";
    string _alias = "";

    //Получаем тег контейнера
    if (tag != null)
        _tag = tag.Val;

    //Получаем ID контейнера
    if (sdtId != null)
        SdtId = _sdtId = sdtId.Val;

    //Получаем название контейнера
    if (alias != null)
        _alias = alias.Val;

    if (_tag.Contains("рисунок"))
    {
        try
        {
            var sdtBlock = wdDoc.MainDocumentPart.Document.Descendants<SdtBlock>()
                        .Where(r => r.SdtProperties.GetFirstChild<SdtId>().Val == _sdtId);
            if (sdtBlock != null)
            {
                Drawing dr = sdtBlock.First().Descendants<Drawing>().FirstOrDefault();
                if (dr != null)
                {
                    GetSizeOfPictureContainer(dr, ref _contWidth, ref _contHeight);
                }
            }
        }
        catch (Exception ex)
        {
            ErrorHandler(ex);
        }
    }
    return _alias;
}

Описание метода GetSizeOfPictureContainer() привожу в листинге ниже. Этот метод определяет размеры области в документе для дальнейшего пропорционального масштабирования изображения при вставке.
private void GetSizeOfPictureContainer(Drawing d, ref int maxWidth, ref int maxHeight)
{
    Extent imageSizeProps = d.Descendants<Extent>().FirstOrDefault();
    maxWidth = (int)(imageSizeProps.Cx / 9525);
    maxHeight = (int)(imageSizeProps.Cy / 9525);
}

Пропорциональное изменение размеров изображения

Так как вставляемое изображение может быть больше или меньше области для вставки, то необходимо вычислить новые размеры для соблюдения пропорций.
В листинге ниже я привожу пример соответствующего метода:
private static void ResizePictureContainer(Drawing d, int originalWidth, int originalHeight, ref int maxWidth, ref int maxHeight)
{
    Extent imageSizeProps = d.Descendants<Extent>().FirstOrDefault();

    if (imageSizeProps != null)
    {
        int imageWidthOr = (int)(imageSizeProps.Cx / 9525);
        int imageHeightOr = (int)(imageSizeProps.Cy / 9525);
        maxWidth = imageWidthOr;
        maxHeight = imageHeightOr;
        //Определяем соотношение сторон
        double aspectRatio = (double)originalWidth / (double)originalHeight;

        //Проверяем, что высота изображения больше разрешенной высоты
        int newHeight = (originalHeight > imageHeightOr) ? imageHeightOr : originalHeight;
        //Проверяем, что ширина изображения больше разрешенной ширины
        int newWidth = (originalWidth > imageWidthOr) ? imageWidthOr : originalWidth;
        //Вычисляем новую высоту или ширину в зависимости от соотношения сторон (полагаясь на ориентацию изображения)
        if ((newWidth == originalWidth) && (newHeight == originalHeight))
        {
            //Если ширина больше, то ориентация книжная
            if (newWidth > newHeight)
            {
                //Вычисляем новую высоту умножением ширины на соотношение сторон
                newHeight = (int)(imageWidthOr / aspectRatio);
                newWidth = imageWidthOr;
                //в некторых случаях вычисленная высота может быть больше чем разрешенная 
                //поэтому нужно подвести высоту к разрешенной и пересчитать ширину
                if (newHeight > imageHeightOr)
                {
                    newHeight = imageHeightOr;
                    newWidth = (int)(aspectRatio * newHeight);
                }
            }
            else //ориентация портретная
            {
                //Вычисляем новую ширину умножением высоты на соотношение сторон
                newWidth = (int)(aspectRatio * imageHeightOr);
                newHeight = imageHeightOr;
            }
        }
        else //Если исходное изображение меньше, чем контейнер
        {
            if (newWidth > newHeight)
            {
                newHeight = (int)(newWidth / aspectRatio);
                if (newHeight > imageHeightOr)
                {
                    newHeight = imageHeightOr;
                    newWidth = (int)(aspectRatio * newHeight);
                }
            }
            else 
            {
                newWidth = (int)(aspectRatio * newHeight);
            }
        }
        imageSizeProps.Cx = (long)(newWidth * 9525);
        imageSizeProps.Cy = (long)(newHeight * 9525);
    }

    Extents e2 = d.Descendants<Extents>().FirstOrDefault();

    long imageWidthEmu = (long)(originalWidth * 9525);
    long imageHeightEmu = (long)(originalHeight * 9525);
    if (e2 != null)
    {
        e2.Cx = imageWidthEmu;
        e2.Cy = imageHeightEmu;
    }
}

Вставка изображения

Для вставки изображения нужно обратить внимание на два метода:

  • wordprocessingDocument.MainDocumentPart.AddImagePart()
  • ImagePart.FeedData()
Листинг с примером приведен ниже:
ImagePart ip = wordprocessingDocument.MainDocumentPart.AddImagePart(ImagePartType.Jpeg);
using (Stream inStream = openFileDialog2.OpenFile())
{            
 inStream.Position = 0;
 ip.FeedData(inStream);
}

Всеь проект для Visual Studio 2010 можно скачать по ссылке.

Результат работы программы

После запуска программы выбираем подготовленный документ MS Word из локальной папки, выбираем изображение для вставки и сохраняем результат.

Открываем документ и проверяем изображение.

Подведем итоги

Вставкой изображений не исчерпываются возможности работы с документами MS Word через OpenXML. О других применениях мы поговорим в других статьях!

8 комментариев:

  1. Здравствуйте Павел! Спасибо за очень полезную и подробную статью! Я впервые пробую работать с Open XML SDK 2.5 и к сожалению с первого же оператора - открытие документа - не могу пробиться через ошибку The specified package is invalid. The main part is missing. Оператор выглядит так: using (WordprocessingDocument doc = WordprocessingDocument.Open("G:\\ForTestProgram", false))
    Файл в Word 2007 открывается нормально. Подскажите, пожайлуста, что может быть?

    ОтветитьУдалить
    Ответы
    1. Спасибо за отзыв)) попробуйте создать новый word-файл в программе MS Word 2010, и заполнить его произвольным текстом. После этого нужно сохранить файл и закрыть программу MS Word. И уже потом попробовать открыть этот файл в вашей программе.

      Удалить
  2. Спасибо за ответ. Файл создан в Word 2007, Word закрыт, результат тот же. Обязательно Word 2010?

    ОтветитьУдалить
    Ответы
    1. 2007 тоже должен подойти.
      А можете куда-нибудь выложить ваш заархивированный проект и файл Word и ссылку привести здесь?

      Удалить
  3. Word файл - 3 строки текста и два мальеньких рисунка. Текст проекта:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using DocumentFormat.OpenXml;
    using DocumentFormat.OpenXml.Packaging;
    using DocumentFormat.OpenXml.Wordprocessing;

    public static class ContentControlExtensions
    {
    public static IEnumerable ContentControls(
    this OpenXmlPart part)
    {
    return part.RootElement
    .Descendants()
    .Where(e => e is SdtBlock || e is SdtRun);
    }

    public static IEnumerable ContentControls(
    this WordprocessingDocument doc)
    {
    foreach (var cc in doc.MainDocumentPart.ContentControls())
    yield return cc;
    foreach (var header in doc.MainDocumentPart.HeaderParts)
    foreach (var cc in header.ContentControls())
    yield return cc;
    foreach (var footer in doc.MainDocumentPart.FooterParts)
    foreach (var cc in footer.ContentControls())
    yield return cc;
    if (doc.MainDocumentPart.FootnotesPart != null)
    foreach (var cc in doc.MainDocumentPart.FootnotesPart.ContentControls())
    yield return cc;
    if (doc.MainDocumentPart.EndnotesPart != null)
    foreach (var cc in doc.MainDocumentPart.EndnotesPart.ContentControls())
    yield return cc;
    }
    }

    class Program
    {
    static void Main(string[] args)
    {
    using (WordprocessingDocument doc =
    WordprocessingDocument.Open("G:\\ForTestProgram", false))
    {
    foreach (var cc in doc.ContentControls())
    {
    SdtProperties props = cc.Elements().FirstOrDefault();
    Tag tag = props.Elements().FirstOrDefault();
    Console.WriteLine(tag.Val);
    }
    }
    }
    }
    валится на операторе
    using (WordprocessingDocument doc =
    WordprocessingDocument.Open("G:\\ForTestProgram", false))
    сразу при входе

    ОтветитьУдалить
    Ответы
    1. Нужно указать полный путь к файлу с расширением: "G:\\ForTestProgram.docx"

      Внутри цикла я бы написал так: https://drive.google.com/file/d/0B5DBkc-ijRdqa1RuUHhOandZOGc/edit?usp=sharing

      Удалить
    2. Спасибо, ошибка детская, недосмотрела, извините. Все заработало... Когда все новое, ищешь что-то серьезное, а тут... Спасибо за ссылку, буду смотреть.

      Удалить
    3. Пожалуйста, будут еще вопросы - смело задавайте)

      Удалить