昨天提到 .NET PDF 程式庫,事實上 Telerik 的許多程式庫(DevCraft, UI for ASP.NET Core, UI for ASP.NET MVC, UI for ASP.NET AJAX, UI for Blazor, UI for .NET MAUI, UI for Xamarin, UI for WPF, UI for WinForms, UI for Silverlight)有內建文件處理程式庫(Document Processing Libraries),也能處理檔案合併、加浮水印等 PDF 基本操作。因此若已有 Telerik 元件授權,倒也可以考慮用它處理 PDF,就不必另外尋覓程式庫或購買程式元件。

Telerik 的文件模型有個特色是能同時匯出成 PDF、Word、RTF、HTML、Excel 等格式,滿足需支援多種輸出格式的場景,但這篇文章只會聚焦 PDF,以完成建立 PDF 檔、檔案合併及加浮水印等三項常見作業為目標。

Telerik 的 PDF 處理主要靠 RadPdfProcessing 程式庫,.NET Framework 及 .NET 6+ 要參照的程式庫不同,細節可參考文件

我的測試專案採用 .NET 4.7.2,要參照以下程式庫:

程式範例如下:

using System;
using System.Diagnostics;
using System.IO;
using Telerik.Windows.Documents.Fixed.Model;
using Telerik.Windows.Documents.Fixed.Model.Editing;
using Telerik.Windows.Documents.Fixed.FormatProviders.Pdf;
using Telerik.Windows.Documents.Fixed.Model.ColorSpaces;
using Win = System.Windows;
using Telerik.Windows.Documents.Fixed.Model.Editing.Flow;
using Telerik.Windows.Documents.Fixed.Model.Fonts;
using Telerik.Windows.Documents.Fixed.Model.Editing.Tables;
using System.Windows.Media.Animation;

namespace Net35Pdf
{
    internal class Program
    {
        static string SavePdf(RadFixedDocument doc, bool viewPdf = true)
        {
            var provider = new PdfFormatProvider();
            var tempFilePath = System.IO.Path.GetTempFileName() + ".pdf";
            using (var fs = File.OpenWrite(tempFilePath))
            {
                provider.Export(doc, fs);
            }
            if (viewPdf) Process.Start(new ProcessStartInfo(tempFilePath) { UseShellExecute = true });
            return tempFilePath;
        }

        static void Main(string[] args)
        {
            // 從無到有建立 PDF
            // 方法一,使用 FixedContentEditor,以標準定位繪製文字及圖形
            var document = new RadFixedDocument();
            var page = document.Pages.AddPage();
            var editor = new FixedContentEditor(page);
            // 填滿背景色
            editor.GraphicProperties.FillColor = new RgbColor(0x46,0x82,0xB4); //SteelBlue
            editor.DrawRectangle(new Win.Rect(0, 0, page.Size.Width, page.Size.Height));
            // 繪製灰框白底矩形
            const int margin = 80;
            editor.GraphicProperties.FillColor = RgbColors.White;
            editor.GraphicProperties.StrokeColor = new RgbColor(0x80, 0x80, 0x80);
            editor.GraphicProperties.StrokeThickness = 4;
            editor.DrawRectangle(new Win.Rect(margin, margin, page.Size.Width - 2 * margin, 150));
            // 繪製文字
            var block = new Block();
            block.HorizontalAlignment = HorizontalAlignment.Center;
            block.VerticalAlignment = VerticalAlignment.Center;
            block.TextProperties.FontSize = 28;
            block.GraphicProperties.FillColor = new RgbColor(0x0, 0x0, 0x8b);
            block.InsertText("THIS IS A BOOK");
            // 位移至指定位置繪製文字
            editor.Position.Translate(margin, margin);
            editor.DrawBlock(block, new Win.Size(page.Size.Width - 2 * margin, 150));
            var cover = SavePdf(document, false);

            // REF: https://github.com/telerik/document-processing-sdk/blob/master/PdfProcessing/CreatePdfUsingRadFixedDocumentEditor/Program.cs
            document = new RadFixedDocument();
            // 方法二,用 RadFixedDocumentEditor 以 Paragraph、Run 結構建立文件
            using (var fixEditor = new RadFixedDocumentEditor(document))
            {
                fixEditor.InsertParagraph();
                fixEditor.CharacterProperties.FontSize = 20;
                FontsRepository.TryCreateFont(new Win.Media.FontFamily("Segoe UI"), Win.FontStyles.Normal, Win.FontWeights.Bold, out var font);
                fixEditor.CharacterProperties.Font = font;
                fixEditor.InsertRun("Header 1");
                fixEditor.InsertLineBreak();

                fixEditor.InsertParagraph();
                fixEditor.CharacterProperties.FontSize = 18;
                fixEditor.CharacterProperties.Font = FontsRepository.Helvetica;
                fixEditor.InsertRun("Hello World!");
                // 測試繪製表格
                var tbl = new Table();
                var border = new Border(1, new RgbColor(0x80, 0x80, 0x80));
                tbl.Borders = new TableBorders(border);
                // 指定左上右下四個邊框
                tbl.DefaultCellProperties.Borders = new TableCellBorders(border, border, border, border);
                tbl.DefaultCellProperties.Padding = new Win.Thickness(5);
                Action<TableRow, string> insertCellText = (targetRow, text) =>
                {
                    var blk = targetRow.Cells.AddTableCell().Blocks.AddBlock();
                    blk.TextProperties.FontSize = 16;
                    blk.InsertText(text);
                };
                var row = tbl.Rows.AddTableRow();    
                insertCellText(row, "Cell 1");
                insertCellText(row, "Cell 2");
                insertCellText(row, "Cell 3");
                row = tbl.Rows.AddTableRow();
                insertCellText(row, "Cell 4");
                insertCellText(row, "Cell 5");
                insertCellText(row, "Cell 6");

                fixEditor.InsertTable(tbl);
            }
            var content = SavePdf(document, false);

            // 合併 PDF 
            PdfFormatProvider provider = new PdfFormatProvider();
            RadFixedDocument mergedDoc = provider.Import(File.ReadAllBytes(cover));
            mergedDoc.Merge(provider.Import(File.ReadAllBytes(content)));

            // 為合併後的文件加上浮水印
            // https://github.com/telerik/document-processing-sdk/blob/master/PdfProcessing/ManipulatePages/Program.cs
            foreach (var pg in mergedDoc.Pages)
            {
                var blk = new Block();
                blk.GraphicProperties.FillColor = new RgbColor(127, 255, 0, 0);
                blk.TextProperties.FontSize = 96;
                blk.InsertText("TOP SECRET");
                Win.Size wmSize = blk.Measure();
                var squareSize = (wmSize.Width + wmSize.Height) / Math.Sqrt(2);

                var ed = new FixedContentEditor(pg);
                ed.Position.Rotate(-45);
                var nx = (pg.Size.Width - squareSize) / 2;
                var ny = (pg.Size.Height - squareSize) / 2 + (wmSize.Width / Math.Sqrt(2));
                ed.Position.Translate(nx, ny);
                ed.DrawBlock(blk);
            }
            var merged = SavePdf(mergedDoc);
        }
    }
}

測試完畢。


Comments

# by fredli

公司不少系統有產生PDF的需要,考量未來可以最低成本抽換套件,因此不想綁死特定套件(譬如最近i開頭的那套件, 我想您應該是知道的)。 所以打算對PDF套件進行抽象化封裝或隔離。 目前唯一想到最比較可行的是透過html去轉PDF。PS:僅考慮server用的,故忽略Puppeteer之類的。 無奈具html轉換功能的套件已經稀少,而這些套件對HTML支援程度以及渲染邏輯不一。 很難以用一份html就可以在不同的套件中切換。 不知黑大您有什麼想法或建議呢?

# by Jeffrey

to fredli, 若情境為單純的 HTML 轉 PDF,我心中的首選是微軟推出的 Playwright https://blog.darkthread.net/blog/playwright-notes/ ,它可使用 Chromium 核心,不用擔心 HTML 支援跟渲染邏輯(網頁用 Chrome 看不正常,錯的應該是網頁吧),而身為 E2E 測試解決方案,我直覺它有能力在伺服器端執行(不然很難配合 CI 運轉),這個議題有點意思,找時間來研究一下。

Post a comment