【问题标题】:Replace image in word doc using OpenXML使用 OpenXML 替换 word 文档中的图像
【发布时间】:2020-02-15 01:27:18
【问题描述】:

接着我的上一个问题here

OpenXML 看起来可能完全符合我的要求,但文档很糟糕。一个小时的谷歌搜索并没有让我更接近于弄清楚我需要做什么。

我有一个word文档。我想以这样一种方式将图像添加到该 word 文档(使用 word),然后我可以在 OpenXML 中打开该文档并替换该图像。应该够简单了吧?

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

我能找到的每一个做任何事情都非常相似的例子都是从在 ML 中从头开始构建整个 word 文档开始的,这真的没什么用处。

编辑:我突然想到用新图像替换媒体文件夹中的图像会更容易,但再次找不到任何指示如何执行此操作。

【问题讨论】:

    标签: .net ms-word openxml


    【解决方案1】:

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

    【讨论】:

    • 亚当,感谢您的出色回答。在您发布此内容之前,我已经设法让一些工作正常进行,因此我在下面自己的答案中添加了更多信息。
    • 第二个代码块是迄今为止我发现的最容易替换图像而不是添加新图像的代码块。如果可以的话,我会投票 2 倍!
    • 谢谢!这个答案现在已经 11 岁了,今天仍然帮助我替换 PowerPoint 演示文稿中的图像。
    【解决方案2】:

    为了其他人的利益,我想更新此线程并添加到亚当的上述答案中。

    实际上,前几天我设法一起破解了一些工作代码(在亚当发布他的答案之前),但这非常困难。文档确实很差,而且那里没有很多信息。

    我不知道 Adam 在他的回答中使用的 InlineRun 元素,但诀窍似乎在于获取 Descendants&lt;&gt; 属性,然后您几乎可以解析任何元素,例如正常的 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>()
        }
    }
    

    一旦你有了这个,搜索东西就相当容易了,尽管你必须弄清楚所有东西都叫什么。比如&lt;pic:nvPicPr&gt;就是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 示例以了解此处的不同元素并查看我要搜索的内容。

    blip 在Embed 属性中有一个 ID,例如:&lt;a:blip r:embed="rId4" cstate="print" /&gt;,它的作用是将 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);
    

    我认为这只是孤立了 Media 文件夹中的旧图像,这并不理想,尽管可以这么说它可能足够聪明地垃圾收集它。可能有更好的方法,但我找不到。

    无论如何,你有它。这个线程现在是关于如何在网络上任何地方交换图像的最完整的文档(我知道这一点,我花了几个小时搜索)。所以希望有些人会发现它很有用。

    【讨论】:

    • 看起来不错,很高兴你能成功。我使用 Inline 的原因是我正在查看 的图像文件名,它是 Inline 的子元素。不过,您的解决方案更有意义,因为文件名也在 元素中。
    • 晚会有点晚了,但我只是想感谢亚当和你。节省了我很多时间试图弄清楚如何替换占位符图像并浏览 Open XML 文档
    【解决方案3】:

    在我看到这个帖子之前,我试图弄清楚如何做到这一点也很有趣。非常有用的答案,伙计们。

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

    ImagePart GetImagePart(WordprocessingDocument document, string imageName) { return document.MainDocumentPart.ImageParts .Where(p => p.Uri.ToString().Contains(imageName)) // or EndsWith .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); }

    【讨论】:

      【解决方案4】:

      以下代码将从指定文档(文件名)中检索图像,并使用内部文件名将它们保存到 D:\TestArea 文件夹。此页面上的答案帮助我想出了我的解决方案。

      注意:此解决方案不能帮助某人替换 word doc 中的图像,但是在我所有关于如何从 word doc 中检索图像的搜索中,这是我能找到的唯一/最接近的链接;以防万一其他人在同一条船上,我在这里发布我的解决方案。

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

      【讨论】:

        【解决方案5】:

        我喜欢这个部分,因为关于这个主题有很多糟糕的文档,并且经过数小时的尝试使上述答案有效。我想出了自己的解决方案。

        我如何给图像一个标签名:

        首先,我选择要在 word 中替换的图像并给它一个名称(例如“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));
                        }
                    }
                }
            }
        }
        

        【讨论】:

        • 确实,这个有效。在我的情况下,只是文档的标记不同。我不得不使用dpr.Title
        【解决方案6】:

        @Ludisposed excellent answer 非常适合我,但我花了一点时间才弄清楚如何在 Word 中实际设置图像名称。对于不会说德语的其他人,这是如何做到的:

        在 MS Word 中,单击图像,然后在主页功能区中,选择功能区中的选择 -> 选择窗格以在右侧导航中显示图像列表:

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

        完成后,您可以使用 Open XML SDK 2.5 Productivity Tool 查看该文本是如何合并到 Open XML 文件中的:

        完成后,我将@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();
                }
            }
        }
        

        【讨论】:

          【解决方案7】:

          为了获取图像并将它们复制到文件夹中,您可以使用更简单的方法

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

          【讨论】:

            【解决方案8】:

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

            【讨论】:

              【解决方案9】:

              好的,感谢所有帮助我的人。我的目标比替换图像更简单,主要是拉出 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]
              };
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2012-07-23
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多