• [转]为ReportViewer导出的PDF文档加上水印


    接到一個頗富挑戰性的需求,Reporting Service或RDLC報表可匯出成Excel、PDF等檔案格式,對一般麻瓜型使用者而言,PDF唯讀,Excel則可修改,業務單位希望在拿到報表紙本時加以區分;換句話說,如果能讓PDF與Excel檔的列印結果有別,即可做為報表結果是否唯讀,有無被修改可能的依據。(姑且排除使用者設法修改PDF檔或將Excel仿製成PDF樣式的情境)

    我想到一個做法是為匯出的PDF檔加上浮水印。同一張報表匯出的Word、Excel、PDF檔內容理應一致,當PDF檔被加註浮水印,便足以形成區隔。在PDF檔加上浮水印非難事,用iTextSharp應可搞定,但匯出PDF檔的過程本屬ReportViewer內部運作,不容外人插手,要在匯出PDF檔時動手腳需要點Hacking,好一個讓程式魔人熱血沸騰的挑戰!

    分析問題的第一步,先剖析ReportViwer匯出PDF檔的原理:

    當執行PDF匯出動作時,實際上是呼叫ReportViewer加掛的HttpHandler,web.config可以看到相關設定:

    <add path="Reserved.ReportViewerWebControl.axd" verb="*" type="Microsoft.Reporting.WebForms.HttpHandler, Microsoft.ReportViewer.WebForms, Version=11.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" validate="false" />

    在網頁按匯出鈕時,瀏覽器被導向特定URL,傳入OpMode=Export、Format=PDF參數,由HttpHandler傳回當下檢視報表的PDF檔。如要在此過程動手腳,有個不錯的切入點是透過Global.asax或HttpModule攔截BeginRequest事件,遇到呼叫Reserved.ReportViewerWebControl.axd匯檔案時加入自訂邏輯,修改要傳回的檔案內容。但ReportViewer的HttpHandler在PDF檔產生後便立刻寫入HttpRepsonse傳回客戶端,輪不到我們插手,因此下一個挑戰是如何攔截並修改其內容。

    此時,ASP.NET的另一個好用機制派上用場: Response.Filter,它允許我們在HttpResponse將結果byte[]寫入輸出Stream之前,先交由我們自訂的Stream物件處理,可以實現修改後再傳至客戶端的目的。

    排版顯示純文字
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Web;
     
    public class ExpFileFilterStream : MemoryStream
    {
        private Stream output = null;
        Func<byte[], byte[]> modifier = null;
        private HttpResponse response = null;
        private bool firstFlush = false;
        public ExpFileFilterStream(HttpResponse resp, Func<byte[], byte[]> modifier)
        {
            response = resp;
            output = resp.Filter;
            this.modifier = modifier;
        }
        public override void Write(byte[] buffer, int offset, int count)
        {
            //由於ReportViewer會關閉BufferOutput,並分成多段Flush傳回前端,
            //在此重新啟用Buffer功能(因必須得到檔案完整內容再處理),
            //但會漏掉第一次的Flush(),藉以以下邏輯避免第一次部分Flush()
            //註: ReportViewer在分段Flush的大小為81920,當少於此值表示不需略過Flush
            if (!response.BufferOutput && count == 81920)
            {
                response.BufferOutput = true;
                firstFlush = true;
            }
            base.Write(buffer, offset, count);
        }
        public override void Flush()
        {
            if (firstFlush)
            {
                firstFlush = false;
                return;
            }
            //Flush時,將要傳回內容byte[]交由外部邏輯處理後再取回
            byte[] buff = base.ToArray();
            if (modifier != null)
                buff = modifier(buff);
            output.Write(buff, 0, buff.Length);
        }
    }

    我寫了一個簡單的Filter Stream物件,原理是在Write()時先蒐集ReportViewer HttpHandler要傳回的檔案內容,當Flush()要傳回結果時,將先前接收到的PDF檔案內容(byte[])交由外部邏輯,Func<byte[], byte[]>,進行加蓋浮水印處理,再傳回修改版檔案到真正的OutputStream。

    其中有小技巧: ReportViewer HttpHandler為了減少記憶體耗用及提高回應效率,會將Response.BufferOutput設為false,讓匯出檔案內容分成多段Flush()傳回(每段不超過81920 bytes)。由於我們需要接收完整檔案才能進行修改並一次回傳,故不容先傳回部分未修改內容的情形發生。在Write()將Response.BufferOutput改回true即可偷偷取消分段傳回,唯此時第一個分段的Flush()已箭在弦上,故要用一個firstFlush旗標避開第一次Flush()。之後因Response.BufferOutput已被設為true,會等到全部的PDF檔都透過Write()寫入才呼叫Flush(),此時MemoryStream所保存的便是完整PDF檔內容。

    在BeginRequest事件加掛Response.Filter的工作,則寫成一個HttpModule。程式很單純,較花工夫的是透過iTextSharp在PDF左上角印上PDF yyyy/MM/dd HH:mm:ss樣式的半透明浮水印,iText歷史悠久、功能強大,網路上不難找到現成範例,很順利就完成了。

    排版顯示純文字
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Text;
    using System.Web;
    using iTextSharp.text.pdf;
    using iText = iTextSharp.text;
    using iTextSharp.text;
    using System.IO;
     
    namespace ReportViewerHacking
    {
        public class WaterMarkModule : IHttpModule
        {
            #region IHttpModule Members
     
            public void Dispose()
            {
            }
     
            public void Init(HttpApplication context)
            {
                context.BeginRequest += context_BeginRequest;
            }
     
            void context_BeginRequest(object sender, EventArgs e)
            {
                HttpApplication app = sender as HttpApplication;
                string url = app.Context.Request.RawUrl;
                var context = app.Context;
                if (url.Contains("Reserved.ReportViewerWebControl.axd"))
                {
                    var req = context.Request;
                    var resp = context.Response;
                    string opType = req["OpType"];
                    string name = req["Name"];
                    string format = req["Format"];
                    if (opType == "Export" && format == "PDF")
                    {
                        resp.BufferOutput = true;
                        resp.Filter = new ExpFileFilterStream(resp, (buff) =>
                        {
                            //輸入PDF內容,加上浮水印
                            PdfReader pr = new PdfReader(buff);
                            iText.Rectangle dimension = pr.GetPageSize(1);
                            MemoryStream ms = new MemoryStream();
                            PdfStamper stmp = new PdfStamper(pr, ms);
                            //REF: http://bit.ly/10qirzK
                            BaseFont bf =
                                BaseFont.CreateFont(BaseFont.TIMES_ROMAN, BaseFont.CP1252, false);
                            iText.Font fnt = new iText.Font(bf, 6, iText.Font.NORMAL, BaseColor.BLACK);
                            PdfContentByte cb = stmp.GetOverContent(1);
                            //設定半透明文字
                            PdfGState gstate = new PdfGState();
                            gstate.FillOpacity = 0.2f;
                            gstate.StrokeOpacity = 0.2f;
                            cb.SetGState(gstate);
                            cb.BeginText();
                            cb.SetFontAndSize(bf, 6);
                            cb.SetColorFill(BaseColor.BLACK);
                            cb.ShowTextAligned(PdfContentByte.ALIGN_LEFT,
                                string.Format("PDF {0:yyyy-MM-dd HH:mm:ss}", DateTime.Now),
                                dimension.GetLeft(1), dimension.GetTop(5), 0);
                            cb.EndText();
     
                            stmp.Close();
                            pr.Close();
                            return ms.ToArray();
                        });
                    }
                }
            }
            #endregion
        }
    }

    將HttpModule掛進ASP.NET網站,之後只要ReportViewer匯出PDF檔,就一律會被偷偷加上浮水印,讓我過了小小當駭客的癮,哈!!

  • 相关阅读:
    hdu 2881 Jack's struggle(DP)
    [置顶] DataGridView控件---绑定数据方法
    Computational Geometry Template_Polygon
    Max retries exceeded with url
    Linux /proc 的意义
    [置顶] 生成树协议介绍
    StringBuffer与StringBuilder的异同
    linux 下Eclipse for C/C++的不常见设置
    [Elasticsearch] 分布式搜索
    oracle递归函数
  • 原文地址:https://www.cnblogs.com/bluewhale84/p/4250845.html
Copyright © 2020-2023  润新知