使用OpenXML替换Word文档中的图片

31

接着我的上一个问题 (点击此处)

OpenXML 看起来可能正是我想要的,但是文档很糟糕。搜了一个小时也没能找到更多信息。

我有一个 Word 文档。我想在该 Word 文档中添加一张图片(使用 Word),以便我可以在 OpenXML 中打开该文档并替换该图片。应该很简单,对吧?

我假设我应该能够给我的图像“占位符”分配某种 ID,然后使用 GetPartById 来定位该图像并替换它。这是正确的方法吗?这个 ID 是什么?如何在 Word 中添加它?

我能找到的每个类似操作的示例都是从头开始使用 ML 构建整个 Word 文档,这真的没有太大用处。

编辑:我意识到直接用新图像替换媒体文件夹中的图像会更容易,但同样找不到如何做到这一点的任何指示。

9个回答

40
虽然OpenXML的文档不是很好,但有一个出色的工具可以用来查看现有Word文档的构建方式。如果您安装了OpenXml SDK,它将带有位于Open XML Format SDK\V2.0\tools目录下的DocumentReflector.exe工具。
Word文档中的图像由图像数据和分配给它的ID组成,在文档正文中引用该ID。看起来您的问题可以分解为两个部分:查找文档中图像的ID,然后为其重新编写图像数据
要查找图像的ID,您需要解析MainDocumentPart。图像存储在Runs中作为绘图元素。
<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元素中的图像的ID。如何查找取决于您的问题,但如果您知道原始图像的文件名,可以查看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;
  }
}

然后当你有了图片ID之后,你可以使用它来重写图片数据。我认为这是你应该做的:

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();

Adam,感谢你的出色回答。在你发布这个回答之前,我已经设法让一些东西运作起来了,所以我在下面添加了一些更多的信息。 - fearofawhackplanet
第二个代码块是我目前为止找到的最容易替换图像而不是添加新图像的方法。如果可以的话,我会投2票! - Alexander Derck
1
谢谢!这个回答已经有11年了,但今天仍然帮助我替换PowerPoint演示文稿中的图片。 - Tobi o' Bobi

20

为了帮助其他人,我想更新这个帖子并在Adam上面的答案中添加一些内容。

实际上,我在Adam发表他的答案之前几天就成功地编写出了一些可工作的代码,但是它相当困难。文件确实很差,没有太多的信息可以查询。

我不知道Adam在他的答案中使用了InlineRun元素,但技巧似乎在于获取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等等。

正如Adam所说,你需要找到要替换图像的Blip元素。但是,您需要找到相应于您尝试替换的图像的正确blip。

Adam展示了一种使用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
}

可以查看Adam的XML示例,以理解这里的不同元素,并了解我正在搜索的内容。

Embed属性中,该"blip"具有一个ID,例如:<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" />

因此,您需要添加一个新的图像,然后将此"blip"指向您新创建的图像的ID。

// 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);

我猜这只是将旧图像留在媒体文件夹中,这并不理想,尽管它可能足够聪明地进行垃圾收集。也许有更好的方法,但我找不到。

无论如何,这就是你想要的。这个帖子现在是如何在网络上的任何地方交换图像的最完整文档(我知道这一点,我花了几个小时搜索)。因此,希望一些人会发现它有用.


1
看起来很不错,很高兴你把它搞定了。我使用Inline的原因是我在查看图像文件名的wp:docPr时,它是Inline的子元素。不过你的解决方案更有意义,因为文件名也在pic:cNvPr元素中。 - Adam Sheehan
有点晚了,但我想感谢Adam和你。这节省了我很多时间,试图找出如何替换占位符图片并浏览Open XML文档。 - Gilles Radrizzi

9

我曾经也想尝试如何做到这一点,直到看到了这个帖子。大家都给出了非常好的有用答案。

如果你知道包中图像的名称,一种简单的方法是检查Uri来选择ImagePart。


ImagePart GetImagePart(WordprocessingDocument document, string imageName)
{
    return document.MainDocumentPart.ImageParts
        .Where(p => p.Uri.ToString().Contains(imageName)) // 或者以什么结尾
        .First();
}

然后你可以这样做:


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);
}


5
我喜欢这个部分,因为关于这个主题的文档很差,经过多个小时尝试让上述答案起作用后,我想出了自己的解决方案。
如何为图片设置标签名:

enter image description here

首先我选择我想要替换的图像,并给它一个名称(例如“toReplace”),然后我循环遍历绘图,选择具有正确标记名称的图像,并用自己的图像替换它。

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
确实,这个可以工作。只是在我的情况下文档的标记不同。我必须使用 dpr.Title - pxp

4
以下代码将从指定文档(文件名)中检索图像,并使用内部文件名将它们保存到 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);
            }
        }
    }
}

3
@Ludisposed excellent answer 对我非常有效,但我花了一些时间才弄清楚如何在Word中设置图像名称。对于任何其他不会讲德语的人,这就是如何操作的:
在 MS Word 中,单击图像,然后在主页选项卡中,选择“选择” -> “选择窗格”,以显示右侧导航中的图像列表:”

MS Word Selection Pane

您可以在选择窗格中单击图像的名称/标签以更改其名称:

Changing an Image name in the selection pane in MS Word

一旦你完成了这个步骤,你就可以使用Open XML SDK 2.5 Productivity Tool查看这段文本是如何被整合到Open XML文件中的:

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
为了获取图片并将其复制到文件夹中,您可以使用更简单的方法。
        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();

        }

1

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();       
        }

0

好的,非常感谢所有帮助我的人。我的目标比更换图像要简单,主要是从Word文档中提取所有图像。我发现这段代码可以为我完成这项工作,包括所需的扩展名。

请随意使用:

var inlineImages = from paragraph in wordprocessingDocument.MainDocumentPart.Document.Body
  from graphic in paragraph.Descendants<DocumentFormat.OpenXml.Drawing.Graphic>()
  let graphicData = graphic.Descendants<DocumentFormat.OpenXml.Drawing.GraphicData>().FirstOrDefault()
  let pic = graphicData.ElementAt(0).Descendants<DocumentFormat.OpenXml.Drawing.Blip>().FirstOrDefault()
  let imgPID = pic.GetAttribute("embed", "http://schemas.openxmlformats.org/officeDocument/2006/relationships").Value
  select new { Id = imgPID,
               Extension = ((ImagePart)wordprocessingDocument.MainDocumentPart.GetPartById(imgPID)).ContentType.Split('/')[1]
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接