分享 Coding4Fun 專案裡的新嘗試 - 在 Linux 跑 ASP.NET Core 動態產生 Word 檔! 聽起來很酷吧?

OpenXML SDK 從 2.7.0 (2017/1) 起支援 .NET Standard 1.3 (延伸閱讀:.NET Standard 2.0 是什麼?可以吃嗎?), 換言之,OpenXML SDK 已支援 .NET Core,可跨平台在 Linux 執行沒問題。

過去我有一些用 OpenXML SDK 處理 Word 的經驗:

但建立表格是第一次,放在 Linux 跑更是破天荒。

構想是這樣的。我想依據傳入的英文單字陣列產生一張測驗卷,題目以表格排列,每個單字有英文題目與中文填空兩欄,每列放兩個單字共四欄。 我先做了個範本 Word,隨意放上頁首圖示、標題與頁碼,搞定邊距、字型大小等排版設定,這部分程式也能處理,但 GUI 人工處理直覺省事一些:

要依據範本產生新文件,無腦做法是將範本複製成新檔進行編輯,但在伺服器產生暫存檔需打通存取權限並有事後刪除問題,全程在記憶體裡做完才是王道, 而 OpenXML SDK 支援使用 MemoryStream 的不落地式解法。微軟的官方文件https://docs.microsoft.com愈來愈完整好讀, 靠著兩篇範例就寫出我要的功能:

完整程式碼如下:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using Microsoft.AspNetCore.Hosting;

namespace EngVoc7000.Model
{
    public class WordMaker
    {
        public static byte[] GenVocQuizPaper(string tmplPath, string[] vocList)
        {
            var tmplContent = File.ReadAllBytes(tmplPath);
            using (var ms = new MemoryStream())
            {
                ms.Write(tmplContent, 0, (int)tmplContent.Length);
                using (var doc = WordprocessingDocument.Open(ms, true))
                {
                    
                    var tbl = new Table();
                    //設定框線
                    var tp = new TableProperties(
                        //指定田字形六條線的樣式及線寬
                        new TableBorders(
                            //Size 單位為 1/8 點 [註]
                            new TopBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 8 },
                            new BottomBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 8 },
                            new LeftBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 8 },
                            new RightBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 8 },
                            new InsideHorizontalBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 8 },
                            new InsideVerticalBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 8 }
                       )
                    );
                    tbl.AppendChild<TableProperties>(tp);
                    var que = new Queue<string>(vocList);
                    //A4 直式,兩欄
                    while (que.Any())
                    {
                        var tr = new TableRow();
                        //每一列放四欄
                        for (var i = 0; i < 2; i++)
                        {
                            var tc = new TableCell();
                            //第一欄為單字
                            tc.Append(new TableCellProperties(new TableCellWidth()
                            {
                                //寬度取15%
                                Type = TableWidthUnitValues.Pct,
                                Width = "15"
                            }));
                            var text = que.Any() ? que.Dequeue() : string.Empty;
                            tc.Append(new Paragraph(new Run(new Text(text))));
                            tr.Append(tc);
                            //第二欄為填空
                            tc = new TableCell();
                            tc.Append(new TableCellProperties(new TableCellWidth()
                            {
                                Type = TableWidthUnitValues.Pct,
                                Width = "35"
                            }));
                            tc.Append(new Paragraph(new Run(new Text(string.Empty))));
                            tr.Append(tc);
                        }
                        tbl.Append(tr);
                    }
                    doc.MainDocumentPart.Document.Body.Append(tbl);
                }
                return ms.ToArray();
            }            
        }
    }
}

【註】OpenXML 使用的尺寸單位比較特別,包含:(參考:Points, inches and Emus: Measuring units in Office Open XML)

  • dxa (Twentieths of a point),代表一個點( Point )的二十分之一,每一英吋有 72 個點。
  • Half-Points,用於字型,12pt 等於 24 half-points
  • Pct,百分比,用於表格寬度、欄位寬度及邊距(Margin)
  • EMU,English Metric Unit,用於向量繪圖及內嵌圖,1 吋 = 914400 EMUs

但線寬單位不在其中,在 MSDN 論壇有人提到線寬以 1/8 點( 1/8 Points Unit)為單位(24=3 Points),但我沒找到精確術語或定義,若有人知道請留言指點。

輸出成品如下:

OpenXML SDK 對我不是什麼新鮮事兒,但用 C# 在 Linux 主機生出 Word 文件感覺就是爽,哈!

Sample code of generating Word table with .NET Core.


Comments

# by LYM

請問一下有從樣板修改WordprocessingDocument文字 但不複製複本直接寫入memorystream的作法嗎 這兩天自己試了也去找資料 但都只找到會直接修改到原檔案或一定得弄出個複本來修改的 不知道有無方法不用寫複本 直接把修改後的內容寫入memorystream直接Response給人

# by Jeffrey

to LYM,這篇文章示範的做法就是將範本讀成byte[],寫入MemoryStream的做法,過程不會改到原範本檔,也不需要另存實體複本檔案。是否滿足你的需求?

# by LYM

抱歉資質駑鈍 雖有順利讀取到MemoryStream 但對於修改裡面文字還是不知從何下手 看裡面屬性有個MainDocumentPart.Document.InnerText 但卻受保護無法修改

# by LYM

現在的問題在於網路上能看到取代文字的範例(包含微軟) 都是做出個複本 在複本上動手 (用StreamReader讀出資料 修改用再用StreamWrite寫入) https://docs.microsoft.com/en-us/office/open-xml/how-to-search-and-replace-text-in-a-document-part 將範本寫入MemoryStream裡的 卻找不到可以取代文字的方法

# by LYM

在剛剛無助完後 拿微軟的範例與StackOverFlow的一湊 居然成功了... 就是在用一個StreamReader讀出文字取代後 再用MainDocumentPart.FeedData(stream)塞回去 謝謝您的幫忙

# by Jeffrey

to LYM, Good for you!

# by Ted

謝謝黑大您這經典範例分享,輸出的docx在client的word檔開啟完美沒問題,但我嘗試在office 365確產生跑版,不知黑大是否有什麼建議

# by Jeffrey

to Ted, 處理這種相容問題沒什麼捷徑,就是看哪裡跑版調它對映的參數,或是換種兩邊呈現結果一致的寫法,走愚公移山路線。

# by Ted

瞭解,謝謝您的回覆

Post a comment