接到一個頗富挑戰性的需求,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檔,就一律會被偷偷加上浮水印,讓我過了小小當駭客的癮,哈!!

[转]为ReportViewer导出的PDF文档加上水印的更多相关文章

  1. 将w3cplus网站中的文章页面提取并导出为pdf文档

    最近在看一些关于CSS3方面的知识,主要是平时看到网页中有很多用CSS3实现的很炫的效果,所以就打算系统的学习一下.在网上找到很多的文章,但都没有一个好的整理性,比较凌乱.昨天看到w3cplus网站中 ...

  2. rails应用页面导出为pdf文档

    1.下载安装wkhtmltox https://wkhtmltopdf.org/downloads.html   2.gemfile添加 gem 'pdfkit' #页面导出pdf gem 'wkht ...

  3. 如何在PDF文档上加水印

    当我们需要传输一些比较重要的文件时,往往会选择将文档转换为PDF文件,避免其他人复制.更改文档的内容. pdfFactory不仅可以为用户提供快速创建PDF的功能,同时还提供了添加水印的功能.有了水印 ...

  4. ABBYY FineReader 15 中保存和导出PDF文档的小细节

    运用ABBYY FineReader OCR文字识别软件,用户能将各种格式的PDF文档保存为新的PDF文档.PDF/A格式文档,以及Microsoft Word.Excel.PPT等格式.在保存与导出 ...

  5. C#(MVC) Word 替换,填充表格,导出并下载PDF文档

    近期做一个关于C# 操作 Word 模板 文档的功能模块,查阅资料,最终完美完成任务,记录下来,以便后面还会用到.

  6. 使用Spire PDF for .NET将HTML转换成PDF文档

    目录 开发环境说明 Spire PDF for .NET (free edition)体验 资源下载 开发环境说明 Microsoft Visual Studio 2013 Ultimate Edit ...

  7. 利用Java动态生成 PDF 文档

    利用Java动态生成 PDF 文档,则需要开源的API.首先我们先想象需求,在企业应用中,客户会提出一些复杂的需求,比如会针对具体的业务,构建比较典型的具备文档性质的内容,一般会导出PDF进行存档.那 ...

  8. 【Win10 开发】读取PDF文档

    关于用来读取PDF文档的内容的API,其实在Win8.1的时候就有,不过没关系,既咱们讨论的是10的UAP,连同8.1的内容也包括进去,所以老周无数次强调:把以前的内容学好了,就可以在不学习任何新知识 ...

  9. 一起学微软Power BI系列-官方文档-入门指南(7)发布与共享-终结篇+完整PDF文档

    接触Power BI的时间也只有几个月,虽然花的时间不多,但通过各种渠道了解收集,谈不上精通,但对一些重要概念和细节还是有所了解.在整理官方文档的过程中,也熟悉和了解了很多概念.所以从前到后把微软官方 ...

随机推荐

  1. [AH2017/HNOI2017]抛硬币

    传送门 这个题的暴力比较好想--然后用一些组合的知识就可以变成正解了. 首先我们考虑a=b的情况.我们把扔出来的硬币看成是一个01序列,那么对于一个b获胜的序列,他在每一位都按位异或1之后必然是一个a ...

  2. 出现”/var/lib/mysql/mysql.sock“不存在的解决方法

    这种情况大多数是因为你的mysql是使用rpm方式安装的,它会自动寻找 /var/lib/mysql/mysql.sock 这个文件,通过unix socket登录mysql.常见解决办法如下:1.创 ...

  3. 解决向github提交代码不用输入帐号密码

    解决方案:方案一: 在你的用户目录下新建一个文本文件.git-credentials Windows:C:/Users/username Mac OS X: /Users/username Linux ...

  4. 转:Ubuntu12.04编译VLC,在linux上运行

    Ubuntu12.04编译vlc2.1.0 1.编译环境 VM8.0.1 # gcc -v Using built-in specs. COLLECT_GCC=gcc COLLECT_LTO_WRAP ...

  5. web数据存储

    数据的存储必然是任何网站必须经历的事,我们可以将数据存放在不同地方,数据库.文件.内存.程序本身.cookie,session中都可以,但是只要需要持久化保留的数据,那么最终肯定还是落在磁盘之上的,我 ...

  6. 算法导论17:摊还分析学习笔记(KMP复杂度证明)

    在摊还分析中,通过求数据结构的一系列的操作的平均时间,来评价操作的代价.这样,即使这些操作中的某个单一操作的代价很高,也可以证明平均代价很低.摊还分析不涉及概率,它可以保证最坏情况下每个操作的平均性能 ...

  7. CF-831A

    A. Unimodal Array time limit per test 1 second memory limit per test 256 megabytes input standard in ...

  8. 微信小程序开发之三元运算符代替wx.if/wx.else

    直接上代码 实现功能为:当fbphotoFirst为空时,src路径为“pic/信息反馈1-1_14.png“,并且点击事件uploadfbphotoFirst有效,否则为路径fbphotoFirst ...

  9. JavaScript 对象字面量

    JavaScript 对象字面量   JavaScript 对象字面量 在编程语言中,字面量是一种表示值的记法.例如,"Hello, World!" 在许多语言中都表示一个字符串字 ...

  10. jQuery 字母大小写转换

    "ABC".toLowerCase()//转小写 "abc".toUpperCase()//转大写