接到一個頗富挑戰性的需求,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. HihoCoder 1590 : 紧张的会议室(区间最大+离散化)

    时间限制:20000ms 单点时限:2000ms 内存限制:256MB 描述 小Hi的公司最近员工增长迅速,同时大大小小的会议也越来越多:导致公司内的M间会议室非常紧张. 现在小Hi知道公司目前有N个 ...

  2. vmware 克隆linux虚拟机后的网卡修改方法

    VMware虚拟机安装好CentOS6.5系统后,纯净的系统多克隆几份出来方便后期做试验.克隆步骤很简单,克隆后出现的问题是克隆后的网卡MAC地址和原系统MAC地址一样,在局域网内会有冲突,需要更改克 ...

  3. 点分治 [IOI2011]Race

    BZOJ2599. 放板子.利用agc009-D 代码简洁了很多 #include <bits/stdc++.h> #define N 200005 using namespace std ...

  4. ubuntu_ root change to user

    (1)从user用户切换到root用户 不管是用图形模式登录Ubuntu,还是命令行模式登录,我们会发现缺省的用户是user 但是当我们需要执行一些具有root权限的操作(如修还系统文件)时,经常需要 ...

  5. C#控件刷新

    ; ; foreach (string gen in fn_gen) { //MessageBox.Show(gen); Bitmap Bi = new Bitmap(gen); //使用打开的图片路 ...

  6. python 数据可视化

    一.基本用法 import numpy as np import matplotlib.pyplot as plt x = np.linspace(-1,1,50) # 生成-1到1 ,平分50个点 ...

  7. TypeScript完全解读(26课时)_13.TypeScript完全解读-高级类型(2)

    13.TypeScript完全解读-高级类型(2) 高级类型中文网的地址:https://typescript.bootcss.com/advanced-types.html 创建文件并在index. ...

  8. struts2注解的作用

    Struts2注解 1 Struts2注解的作用 使用注解可以用来替换struts.xml配置文件!!! 2 导包 必须导入struts2-convention-plugin-2.3.15.jar包, ...

  9. CSS3:nth-child()伪类选择器,Table表格奇偶数行定义样式

    转自爱设计 原文链接http://www.dangshopex.com/jishufenxiang/WEBkaifajishu/8653.html CSS3的强大,让人惊叹,人们在惊喜之余,又不得不为 ...

  10. java8--List转为Map、分组、过滤、求和等操作

    利用java8新特性,可以用简洁高效的代码来实现一些数据处理~ 定义1个Apple对象: public class Apple { private Integer id; private String ...