使用 Open XML SDK 在 Word 插入圖片

客戶提了需求,套表應用想在文件範本的特定位置插入圖片。花了點時間研究如何用 OpenXML SDK 實現,以下是我的筆記。

Word docx 其實是一個 ZIP 檔,文件主體是一份 XML。如果你有興趣研究,可以將 docx 更名成 zip 解壓縮(或在 docx 按右鍵選單直接用 7-Zip 解開),其中 word 資料夾有一個 document.xml,打開它會發現 Word 文件是由一堆 <w:p> 包 <w:r> 組成,其中 <w:p> 對應到 Open XML 中的 Paragraph,<w:r> 則對應到 Run

例如以下的 Test.docx:

解壓縮 Test.docx 後檢視 word\document.xml,可看到 Paragraph 文字內容被拆得很細,像「圖片插入位置 –> CAT」幾個字就被拆成 6 個 Run,字型顏色大小不同要拆成不同的 Run 無可厚非,但連中文、英文、符號也被獨立切開,甚至「圖片插入」與「位置」被分成兩個 Run。如何拆成 Run 對文件編輯者沒有任何影響,Word 的確可以全權做主,但使用 Open XML SDK 讀取時就得留心其中差異。

Open XML SDK 官方文件有一個完整的插入圖片範例: 如何: 將圖片插入文書處理文件 (開啟 XML SDK),照方煎藥就能在文件末端插入圖片。不過,我遇到的需求還需要調整圖檔大小及插入位置,便試著改寫較有彈性的版本。

我先將圖片內容及設定抽取成獨立類別 ImageData,偵測 附檔名決定 OpenXML ImagePartType(我只打算支援 JPG、PNG、GIF、BMP),由圖檔寬度高度 Pixel 數除以 DPI(預設300) 換算出以公分為單位的預設寬度與高度,但圖片寬高允許自由調整。Open XML 使用 EMU 作為長度單位,使用時公分要乘上 360000 轉成 EMU 以符合 OpenXML 要求。我選擇不直接插入圖片,而是透過一個 GenerateImageRun() 公用函式將圖片轉成 Run,開發者再視需要決定該插入到文件末端、特定位置或置換現有 Run。

    public class ImageData
    {
        public string FileName = string.Empty;
        public byte[] BinaryData;
        public Stream DataStream => new MemoryStream(BinaryData);
        public ImagePartType ImageType
        {
            get
            {
                var ext = Path.GetExtension(FileName).TrimStart('.').ToLower();
                switch (ext)
                {
                    case "jpg":
                        return ImagePartType.Jpeg;
                    case "png":
                        return ImagePartType.Png;
                    case "":
                        return ImagePartType.Gif;
                    case "bmp":
                        return ImagePartType.Bmp;
                }
                throw new ApplicationException($"Unsupported image type: {ext}");
            }
        }
        public int SourceWidth;
        public int SourceHeight;
        public decimal Width;
        public decimal Height;
        public long WidthInEMU => Convert.ToInt64(Width * CM_TO_EMU);
        public long HeightInEMU => Convert.ToInt64(Height * CM_TO_EMU);
        private const decimal INCH_TO_CM = 2.54M;
        private const decimal CM_TO_EMU = 360000M;       
        public string ImageName;
        public ImageData(string fileName, byte[] data, int dpi = 300)
        {
            FileName = fileName;
            BinaryData = data;
            Bitmap img = new Bitmap(new MemoryStream(data));
            SourceWidth = img.Width;
            SourceHeight = img.Height;
            Width = ((decimal)SourceWidth) / dpi * INCH_TO_CM;
            Height = ((decimal)SourceHeight) / dpi * INCH_TO_CM;
            ImageName = $"IMG_{Guid.NewGuid().ToString().Substring(0, 8)}";
        }
        public ImageData(string fileName, int dpi = 300) : 
            this(fileName, File.ReadAllBytes(fileName), dpi)
        {
        }
    }
    public class DocxImgHelper
    {
        public static Run GenerateImageRun(WordprocessingDocument wordDoc, ImageData img)
        {
            MainDocumentPart mainPart = wordDoc.MainDocumentPart;
 
            ImagePart imagePart = mainPart.AddImagePart(ImagePartType.Jpeg);
            var relationshipId = mainPart.GetIdOfPart(imagePart);
            imagePart.FeedData(img.DataStream);
 
            // Define the reference of the image.
            var element =
                 new Drawing(
                     new DW.Inline(
                         //Size of image, unit = EMU(English Metric Unit)
                         //1 cm = 360000 EMUs
                         new DW.Extent() { Cx = img.WidthInEMU, Cy = img.HeightInEMU },
                         new DW.EffectExtent()
                         {
                             LeftEdge = 0L,
                             TopEdge = 0L,
                             RightEdge = 0L,
                             BottomEdge = 0L
                         },
                         new DW.DocProperties()
                         {
                             Id = (UInt32Value)1U,
                             Name = img.ImageName
                         },
                         new DW.NonVisualGraphicFrameDrawingProperties(
                             new A.GraphicFrameLocks() { NoChangeAspect = true }),
                         new A.Graphic(
                             new A.GraphicData(
                                 new PIC.Picture(
                                     new PIC.NonVisualPictureProperties(
                                         new PIC.NonVisualDrawingProperties()
                                         {
                                             Id = (UInt32Value)0U,
                                             Name = img.FileName
                                         },
                                         new PIC.NonVisualPictureDrawingProperties()),
                                     new PIC.BlipFill(
                                         new A.Blip(
                                             new A.BlipExtensionList(
                                                 new A.BlipExtension()
                                                 {
                                                     Uri =
                                                        "{28A0092B-C50C-407E-A947-70E740481C1C}"
                                                 })
                                         )
                                         {
                                             Embed = relationshipId,
                                             CompressionState =
                                             A.BlipCompressionValues.Print
                                         },
                                         new A.Stretch(
                                             new A.FillRectangle())),
                                     new PIC.ShapeProperties(
                                         new A.Transform2D(
                                             new A.Offset() { X = 0L, Y = 0L },
                                             new A.Extents() { 
                                                    Cx = img.WidthInEMU, Cy = img.HeightInEMU }),
                                         new A.PresetGeometry(
                                             new A.AdjustValueList()
                                         )
                                         { Preset = A.ShapeTypeValues.Rectangle }))
                             )
                             { Uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" })
                     )
                     {
                         DistanceFromTop = (UInt32Value)0U,
                         DistanceFromBottom = (UInt32Value)0U,
                         DistanceFromLeft = (UInt32Value)0U,
                         DistanceFromRight = (UInt32Value)0U,
                         EditId = "50D07946"
                     });
            return new Run(element);
        }
    }

有了公用函式,要插入圖片就簡單了,試試將圖片加在文件末端:

static void Main(string[] args)
{
    var workFileName = $"Test-{DateTime.Now:HHmmss}.docx";
    File.Copy("Test.docx", workFileName);
    using (WordprocessingDocument document =
        WordprocessingDocument.Open(workFileName, true))
    {
        var cat2Img = new ImageData("Cat2.png");
        var imgRun = DocxImgHelper.GenerateImageRun(document, cat2Img);
        document.MainDocumentPart.Document.Body.AppendChild(new Paragraph(imgRun));
    }
}

測試成功! 測試圖片尺寸為 300x300,因預設 300 DPI,300 Pixel 等於 1 吋,故變成 2.54cm x 2.54cm 的圖片,置於文件最後一段 Paragraph。

接著再來測試置換現有內容。用 Document.Body.Descendants() 取回 document.xml 所有 XML 節點,如果我們確定 CAT 文字被包在單一 <w:r> 中(小訣竅: 使用純英文並套用不同字型可確保該段文字自成一個 Run),用 LINQ .Single(o => o.Local == "r" && o.InnerText == "CAT") 可找到 CAT 所在的 Run,接著將其 InnerXml 換成圖片的 InnerXml,CAT 文字就變成圖檔囉~ (本例順便示範改變圖片寬高為 1cm x 1cm)

static void Main(string[] args)
{
    var workFileName = $"Test-{DateTime.Now:HHmmss}.docx";
    File.Copy("Test.docx", workFileName);
    using (WordprocessingDocument document =
        WordprocessingDocument.Open(workFileName, true))
    {
        var cat1Img = new ImageData("Cat1.gif")
        {
            Width = 1,
            Height = 1
        };
        var imgRun = DocxImgHelper.GenerateImageRun(document, cat1Img);
                
        //找到 CAT 所在的 Run
        var runCAT = document.MainDocumentPart.Document.Body.Descendants()
            .Single(o => o.LocalName == "r" && o.InnerText == "CAT");
 
        //將 InnerXML 置換成圖片 Run 的 InnerXML
        runCAT.InnerXml = imgRun.InnerXml;
    }
}

測試成功,YA!

歡迎推文分享:
Published 06 November 2017 07:11 AM 由 Jeffrey
Filed under:
Views: 2,599



意見

沒有意見

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<November 2017>
SunMonTueWedThuFriSat
2930311234
567891011
12131415161718
19202122232425
262728293012
3456789
 
RSS
創用 CC 授權條款
【廣告】
twMVC

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication