Заменить изображение в документе Word, используя OpenXML - PullRequest
26 голосов
/ 11 мая 2010

Исходя из моего последнего вопроса здесь

OpenXML выглядит так, как будто он делает именно то, что я хочу, но документация ужасна. Час поиска в Google не приблизил меня к пониманию того, что мне нужно делать.

У меня есть документ Word. Я хочу добавить изображение в этот документ Word (используя слово) таким образом, чтобы я мог затем открыть документ в OpenXML и заменить это изображение. Должно быть достаточно просто, да?

Я предполагаю, что должен иметь возможность присвоить своему изображению "местозаполнитель" некоторый идентификатор, а затем использовать GetPartById, чтобы найти изображение и заменить его. Будет ли это правильный метод? Что это за идентификатор? Как добавить его с помощью Word?

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

РЕДАКТИРОВАТЬ: мне пришло в голову, что было бы проще просто заменить изображение в папке мультимедиа на новое изображение, но снова не могу найти никаких указаний о том, как это сделать.

Ответы [ 8 ]

32 голосов
/ 13 мая 2010

Хотя документация для OpenXML невелика, есть отличный инструмент, который вы можете использовать, чтобы увидеть, как создаются существующие документы Word. Если вы устанавливаете OpenXml SDK, он поставляется с инструментом DocumentReflector.exe в каталоге Open XML Format SDK \ V2.0 \ tools .

Изображения в документах Word состоят из данных изображения и назначенного ему идентификатора, на который есть ссылка в теле документа. Кажется, что ваша проблема может быть разбита на две части: найти идентификатор изображения в документе, а затем переписать данные изображения для него.

Чтобы найти идентификатор изображения, вам нужно проанализировать MainDocumentPart. Изображения хранятся в режиме «Выполнение» как элемент «Рисование»

<w:p>
  <w:r>
    <w:drawing>
      <wp:inline>
        <wp:extent cx="3200400" cy="704850" /> <!-- describes the size of the image -->
        <wp:docPr id="2" name="Picture 1" descr="filename.JPG" />
        <a:graphic>
          <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
            <pic:pic>
              <pic:nvPicPr>
                <pic:cNvPr id="0" name="filename.JPG" />
                <pic:cNvPicPr />
              </pic:nvPicPr>
              <pic:blipFill>
                <a:blip r:embed="rId5" /> <!-- this is the ID you need to find -->
                <a:stretch>
                  <a:fillRect />
                </a:stretch>
              </pic:blipFill>
              <pic:spPr>
                <a:xfrm>
                  <a:ext cx="3200400" cy="704850" />
                </a:xfrm>
                <a:prstGeom prst="rect" />
              </pic:spPr>
            </pic:pic>
          </a:graphicData>
        </a:graphic>
      </wp:inline>
    </w:drawing>
  </w:r>
</w:p>

В приведенном выше примере вам необходимо найти идентификатор изображения, хранящегося в элементе blip. Как вы находите, это зависит от вашей проблемы, но если вы знаете имя файла исходного изображения, вы можете посмотреть на элемент docPr:

using (WordprocessingDocument document = WordprocessingDocument.Open("docfilename.docx", true)) {

  // go through the document and pull out the inline image elements
  IEnumerable<Inline> imageElements = from run in Document.MainDocumentPart.Document.Descendants<Run>()
      where run.Descendants<Inline>().First() != null
      select run.Descendants<Inline>().First();

  // select the image that has the correct filename (chooses the first if there are many)
  Inline selectedImage = (from image in imageElements
      where (image.DocProperties != null &&
          image.DocProperties.Equals("image filename"))
      select image).First();

  // get the ID from the inline element
  string imageId = "default value";
  Blip blipElement = selectedImage.Descendants<Blip>().First();
  if (blipElement != null) {
      imageId = blipElement.Embed.Value;
  }
}

Затем, когда у вас есть идентификатор изображения, вы можете использовать его для перезаписи данных изображения. Я думаю, вот как ты это сделаешь:

ImagePart imagePart = (ImagePart)document.MainDocumentPart.GetPartById(imageId);
byte[] imageBytes = File.ReadAllBytes("new_image.jpg");
BinaryWriter writer = new BinaryWriter(imagePart.GetStream());
writer.Write(imageBytes);
writer.Close();
17 голосов
/ 14 мая 2010

Я хотел бы обновить эту ветку и добавить ответ Адама выше для пользы других.

Мне действительно удалось на днях взломать какой-то рабочий код (до того, как Адам опубликовал свой ответ), но это было довольно сложно. Документация действительно скудная, и там не так много информации.

Я не знал об элементах Inline и Run, которые Адам использует в своем ответе, но, похоже, дело в том, чтобы добраться до свойства Descendants<>, и тогда вы в значительной степени сможете разобрать любой элемент, например нормальное отображение XML.

byte[] docBytes = File.ReadAllBytes(_myFilePath);
using (MemoryStream ms = new MemoryStream())
{
    ms.Write(docBytes, 0, docBytes.Length);

    using (WordprocessingDocument wpdoc = WordprocessingDocument.Open(ms, true))
    {
        MainDocumentPart mainPart = wpdoc.MainDocumentPart;
        Document doc = mainPart.Document;

        // now you can use doc.Descendants<T>()
    }
}

Как только вы это получили, поискать вещи довольно легко, хотя вам нужно разобраться, как все это называется. Например, <pic:nvPicPr> - это Picture.NonVisualPictureProperties и т. Д.

Как правильно говорит Адам, элемент, который вам нужно найти, чтобы заменить изображение, это элемент Blip. Но вам нужно найти правильный блик, соответствующий изображению, которое вы пытаетесь заменить.

Адам показывает способ использования элемента Inline. Я просто нырнул и посмотрел все элементы изображения. Я не уверен, что является лучшим или более надежным способом (я не знаю, насколько непротиворечива структура XML между документами и вызывает ли это нарушение кода).

Blip GetBlipForPicture(string picName, Document document)
{
    return document.Descendants<Picture>()
         .Where(p => picName == p.NonVisualPictureProperties.NonVisualDrawingProperties.Name)
         .Select(p => p.BlipFill.Blip)
         .Single(); // return First or ToList or whatever here, there can be more than one
}

См. Пример XML Адама, чтобы понять различные элементы здесь и посмотреть, что я ищу.

Blip имеет идентификатор в свойстве Embed, например: <a:blip r:embed="rId4" cstate="print" />, это означает, что Blip сопоставляется с изображением в папке Media (вы можете увидеть все эти папки и файлы, если переименуете .docx). в ZIP-архив и распаковать его). Вы можете найти отображение в _rels\document.xml.rels:

<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png" />

Итак, что вам нужно сделать, это добавить новое изображение, а затем навести этот блик на идентификатор вашего вновь созданного изображения:

// add new ImagePart
ImagePart newImg = mainPart.AddImagePart(ImagePartType.Png);
// Put image data into the ImagePart (from a filestream)
newImg .FeedData(File.Open(_myImgPath, FileMode.Open, FileAccess.Read));
// Get the blip
Blip blip = GetBlipForPicture("MyPlaceholder.png", doc);
// Point blip at new image
blip.Embed = mainPart.GetIdOfPart(newImg);

Полагаю, это просто лишает смысла старый образ в папке «Медиа», который не идеален, хотя, может быть, он достаточно умен, чтобы, так сказать, собрать мусор. Возможно, есть лучший способ сделать это, но я не смог его найти.

Во всяком случае, у вас это есть. Эта ветка теперь является самой полной документацией о том, как поменять изображение в любом месте в Интернете (я знаю это, я потратила часы на поиск). Так что, надеюсь, некоторые люди найдут это полезным.

8 голосов
/ 11 июля 2011

Мне было так же интересно пытаться понять, как это сделать, пока я не увидел эту ветку. Отличные полезные ответы, ребята.

Простой способ выбрать ImagePart, если вы знаете имя изображения в пакете, это проверить Uri


ImagePart GetImagePart(WordprocessingDocument document, string imageName)
{
    return document.MainDocumentPart.ImageParts
        .Where(p => p.Uri.ToString().Contains(imageName)) // or EndsWith
        .First();
}

Вы можете сделать

<code>
var imagePart = GetImagePart(document, imageName);
var newImageBytes = GetNewImageBytes(): // however the image is generated or obtained

using(var writer = new BinaryWriter(imagePart.GetStream()))
{
    writer.Write(newImageBytes);
}

3 голосов
/ 30 июля 2013

Следующий код извлечет изображения из указанного документа (имя файла) и сохранит их в папку D: \ TestArea, используя внутренние имена файлов. Ответы на этой странице помогли мне найти решение.

Примечание: это решение не помогает кому-то заменить изображение в документе Word, однако во всех моих поисках того, как извлечь изображение из документа Word, это была единственная / самая близкая ссылка, которую я смог найти; на всякий случай, если кто-то еще находится в той же лодке, я опубликую свое решение здесь.

private void ProcessImages(string filename)
{
    var xpic = "";
    var xr = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";

    using (WordprocessingDocument document = WordprocessingDocument.Open(filename, true)) 
    {
        var imageParts = 
            from paragraph in document.MainDocumentPart.Document.Body
                from graphic in paragraph.Descendants<Graphic>()
                    let graphicData = graphic.Descendants<GraphicData>().FirstOrDefault()
                        let pic = graphicData.ElementAt(0)
                            let nvPicPrt = pic.ElementAt(0).FirstOrDefault()
                            let blip = pic.Descendants<Blip>().FirstOrDefault()
                            select new 
                            {
                                Id = blip.GetAttribute("embed",xr).Value,
                                Filename = nvPicPrt.GetAttribute("name",xpic).Value
                            };

        foreach(var image in imageParts)
        {
            var outputFilename = string.Format(@"d:\TestArea\{0}",image.Filename);
            Debug.WriteLine(string.Format("Creating file: {0}",outputFilename));

            // Get image from document
            var imageData = document.MainDocumentPart.GetPartById(image.Id);

            // Read image data into bytestream
            var stream = imageData.GetStream();
            var byteStream = new byte[stream.Length];
            int length = (int)stream.Length;
            stream.Read(byteStream, 0, length);

            // Write bytestream to disk
            using (var fileStream = new FileStream(outputFilename,FileMode.OpenOrCreate))
            {
                fileStream.Write(byteStream, 0, length);
            }
        }
    }
}
2 голосов
/ 22 марта 2017

Мне нравится этот раздел, потому что на эту тему очень много плохой документации, и после многих часов попыток заставить вышеуказанные ответы работать. Я придумал собственное решение.

Как мне присвоить образу tagName:

enter image description here

Сначала я выбираю изображение, которое я хочу заменить в слове, и присваиваю ему имя (например, "toReplace"), затем я перебираю рисунки, выбираю изображение с правильным именем tagName и пишу свое собственное изображение вместо него.

private void ReplaceImage(string tagName, string imagePath)
{
    this.wordDoc = WordprocessingDocument.Open(this.stream, true);
    IEnumerable<Drawing> drawings = this.wordDoc.MainDocumentPart.Document.Descendants<Drawing>().ToList();
    foreach (Drawing drawing in drawings)
    {
        DocProperties dpr = drawing.Descendants<DocProperties>().FirstOrDefault();
        if (dpr != null && dpr.Name == tagName)
        {
            foreach (DocumentFormat.OpenXml.Drawing.Blip b in drawing.Descendants<DocumentFormat.OpenXml.Drawing.Blip>().ToList())
            {
                OpenXmlPart imagePart = wordDoc.MainDocumentPart.GetPartById(b.Embed);
                using (var writer = new BinaryWriter(imagePart.GetStream()))
                {
                    writer.Write(File.ReadAllBytes(imagePath));
                }
            }
        }
    }
}
1 голос
/ 03 октября 2018

@ Ludisposed отличный ответ отлично сработал для меня, но мне понадобилось немного покопаться, чтобы понять, как на самом деле установить имя изображения в Word. Для тех, кто не говорит по-немецки, вот как это сделать:

В MS Word, нажмите на изображение, затем на ленте Home выберите Select -> Pane Selection на ленте, чтобы отобразить список изображений в правой навигации:

MS Word Selection Pane

Затем вы можете нажать на имя / метку изображения в области выбора, чтобы изменить его имя:

Changing an Image name in the selection pane in MS Word

Как только вы это сделаете, вы сможете увидеть, как этот текст был включен в файл Open XML с помощью инструмента повышения производительности Open XML SDK 2.5:

enter image description here

Сделав это, я немного расширил решение @ Ludisposed в методе многократного использования и настроил код так, чтобы передача в массив нулевых байтов привела к удалению изображения из документа:

/// <summary>
/// Replaces the image in a document with the new file bytes, or removes the image if the newImageBytes parameter is null.
/// Relies on a the image having had it's name set via the 'Selection Pane' in Word
/// </summary>
/// <param name="document">The OpenXML document</param>
/// <param name="oldImagesPlaceholderText">The placeholder name for the image set via Selection in Word</param>
/// <param name="newImageBytes">The new file. Pass null to remove the selected image from the document instead</param>
public void ReplaceInternalImage(WordprocessingDocument document, string oldImagesPlaceholderText, byte[] newImageBytes)
{
    var imagesToRemove = new List<Drawing>();

    IEnumerable<Drawing> drawings = document.MainDocumentPart.Document.Descendants<Drawing>().ToList();
    foreach (Drawing drawing in drawings)
    {
        DocProperties dpr = drawing.Descendants<DocProperties>().FirstOrDefault();
        if (dpr != null && dpr.Name == oldImagesPlaceholderText)
        {
            foreach (Blip b in drawing.Descendants<Blip>().ToList())
            {
                OpenXmlPart imagePart = document.MainDocumentPart.GetPartById(b.Embed);

                if (newImageBytes == null)
                {
                    imagesToRemove.Add(drawing);
                }
                else
                {
                    using (var writer = new BinaryWriter(imagePart.GetStream()))
                    {
                        writer.Write(newImageBytes);
                    }
                }
            }
        }

        foreach (var image in imagesToRemove)
        {
            image.Remove();
        }
    }
}
1 голос
/ 21 июля 2014

Чтобы получить изображения и скопировать их в папку, вы можете использовать более простой метод

        System.Collections.Generic.IEnumerable<ImagePart> imageParts =  doc.MainDocumentPart.ImageParts;

        foreach (ImagePart img in imageParts)
        {
          var uri = img.Uri;
          var fileName = uri.ToString().Split('/').Last();
          var fileWordMedia = img.GetStream(FileMode.Open);
          string imgPath = mediaPath + fileName;//mediaPath it is folder
          FileStream fileHtmlMedia = new FileStream(imgPath, FileMode.Create);
          int i = 0;
          while (i != (-1))
          {
              i = fileWordMedia.ReadByte();
              if (i != (-1))
              {
                  fileHtmlMedia.WriteByte((byte)i);
              }
          }
          fileHtmlMedia.Close();
          fileWordMedia.Close();

        }
0 голосов
/ 22 июля 2014

openXml документация очень скудная, и большинство из них занимают слишком много времени. Я делал конкретную задачу и хочу поделиться решением. Я надеюсь, что это поможет людям, и они сэкономят ваше время. Мне пришлось получить изображение определенного места в тексте, особенно если это объект Run.

 static string RunToHTML(Run r)
       {
            string exit = "";
            OpenXmlElementList list = r.ChildElements;
            foreach (OpenXmlElement element in list)
            {
                if (element is DocumentFormat.OpenXml.Wordprocessing.Picture)
                {
                    exit += AddPictureToHtml((DocumentFormat.OpenXml.Wordprocessing.Picture)element);
                    return exit;
                }
            }

Точнее, мне нужно перевести абзац документа в html-формат.

 static string AddPictureToHtml(DocumentFormat.OpenXml.Wordprocessing.Picture pic)
        {
            string exit = "";
            DocumentFormat.OpenXml.Vml.Shape shape = pic.Descendants<DocumentFormat.OpenXml.Vml.Shape>().First();
            DocumentFormat.OpenXml.Vml.ImageData imageData = shape.Descendants<DocumentFormat.OpenXml.Vml.ImageData>().First();                 
            //style image
            string style = shape.Style;
            style = style.Replace("width:", "");
            style = style.Replace("height:", "");
            style = style.Replace('.', ',');
            style = style.Replace("pt", "");
            string[] arr = style.Split(';');
            float styleW = float.Parse(arr[0]);//width picture
            float styleH = float.Parse(arr[1]);//height picture
            string relationId = imageData.RelationshipId;
            var img = doc.MainDocumentPart.GetPartById(relationId);
            var uri = img.Uri;//path in file
            var fileName = uri.ToString().Split('/').Last();//name picture
            var fileWordMedia = img.GetStream(FileMode.Open);
            exit = String.Format("<img src=\"" + docPath+uri+ "\" width=\""+styleW+"\" heigth=\""+styleH+"\" > ");
            return exit;
        }

uri это путь к картинке в файле .docx, например: "test.docx / media / image.bmp" используя это изображение, чтобы вы могли получить изображение

static void SavePictures(ImagePart img, string savePath)
        {
                var uri = img.Uri;
               var fileName = uri.ToString().Split('/').Last();
                var fileWordMedia = img.GetStream(FileMode.Open);
                string imgPath = savePath + fileName;
                FileStream fileHtmlMedia = new FileStream(imgPath, FileMode.Create);
                int i = 0;
                while (i != (-1))
                {
                    i = fileWordMedia.ReadByte();
                    if (i != (-1))
                    {
                        fileHtmlMedia.WriteByte((byte)i);
                    }
                }
                fileHtmlMedia.Close();
                fileWordMedia.Close();       
        }
...