有一种PDF文本,使用旋转过的字体来作为水印。文件经过密码保护,不能通过编辑的方法去除。
转载请保留这一段文字:charset#cnblogs,谢绝CSDN知乎之流转载

注意:拥有水印并且编辑密码包含的PDF文档可能具有版权保护,本文仅从技术角度讨论可能性。

正常文件可以被打开而且显示无误,使用iText7的渲染引擎来获取渲染项目,通过对目标文本的隐藏来达到去除文字水印的目的。

以下列举了一些使用过程中的注意点和坑:

  • 环境:Windows 11 Home Edition 23H2
  • 机器:Lenovo L490 i5-8265U@1.6GHz 8C16G
  • 软件:.NET 8.0.400, RoslynPad 19.1
  1. 引用itext7, 8.0.5, itext7.bouncy-castle-adapter, 8.0.5, itext7.font-asian, 8.0.5,中间用来解析加密过的PDF,最后解析亚洲文字。

  2. 写一个TextExtractionStrategy继承IEventListener

class TextExtractionStrategy : IEventListener {
    readonly List<ObjectRenderInfo> info;
    public TextExtractionStrategy(List<ObjectRenderInfo> info) => this.info = info;
    public void EventOccurred(IEventData data, EventType type) {
        switch (data) {
            case TextRenderInfo renderInfo:
                info.Add(new ObjectRenderInfo {
                    Text = renderInfo.GetText(),
                    Matrix = renderInfo.GetTextMatrix(),
                    FontName = renderInfo.GetFont(),
                    FontSize = renderInfo.GetFontSize(),
                    Color = renderInfo.GetFillColor(),
                    Width = renderInfo.GetUnscaledWidth()
                });
                break;
            case ImageRenderInfo imageRender:
                var image = imageRender.GetImage();
                info.Add(new ObjectRenderInfo { Image = image.GetImageBytes(), Vector = imageRender.GetStartPoint(), Height = image.GetHeight(), Width = image.GetWidth(), Matrix = imageRender.GetImageCtm() });
                break;
            case PathRenderInfo pathRender:
                var operation = pathRender.GetOperation();
                if (operation != PathRenderInfo.NO_OP) {
                    info.Add(new ObjectRenderInfo {
                        Path = pathRender.GetPath(),
                        Matrix = pathRender.GetCtm(),
                        Width = pathRender.GetGraphicsState().GetLineWidth(),
                        Color = pathRender.GetStrokeColor(),
                        Operation = pathRender.GetOperation(),
                    });
                }
                break;
        }
    }
    public ICollection<EventType> GetSupportedEvents() => new List<EventType> { EventType.RENDER_TEXT, EventType.RENDER_IMAGE, EventType.RENDER_PATH };
}

没啥好说的,注册三种渲染事件,并且在事件回调的时候通过info将传递的内容记录下来。

List<ObjectRenderInfo> info = new(256);
var strategy = new TextExtractionStrategy(info);
var processor = new PdfCanvasProcessor(strategy);
  1. 字体的处理

因为PDF的字体直接使用的路不通,所以使用简单粗暴的映射本地字体文件的方式进行。如果有一些复式字体不考虑。

PdfFont GetFont(string name) {
    var fontName = "SimSun.ttc,0";
    if (name.Contains("SimHei", StringComparison.CurrentCultureIgnoreCase)) fontName = "SimHei.ttf";
    else if (name.Contains("Times", StringComparison.CurrentCultureIgnoreCase)) fontName = "times.ttf";
    else if (name.Contains("FangSong", StringComparison.CurrentCultureIgnoreCase)) fontName = "simfang.ttf";
    else if (name.Contains("DengXian", StringComparison.CurrentCultureIgnoreCase)) fontName = "deng.ttf";
    else if (name.Contains("Arial", StringComparison.CurrentCultureIgnoreCase)) fontName = "arial.ttf";
    else if (name.Contains("Verdana", StringComparison.CurrentCultureIgnoreCase)) fontName = "Verdana.ttf";
    else if (name.Contains("KaiTi", StringComparison.CurrentCultureIgnoreCase)) fontName = "simkai.ttf";
    else if (name.Contains("Cambria", StringComparison.CurrentCultureIgnoreCase)) fontName = "Cambria.ttc,0";
    else if (name.Contains("YuGothic", StringComparison.CurrentCultureIgnoreCase)) fontName = "YuGothL.ttc,0";
    else if (name.Contains("Calibri", StringComparison.CurrentCultureIgnoreCase)) fontName = "Calibri.ttf";
    else if (name.Contains("CourierNew", StringComparison.CurrentCultureIgnoreCase)) fontName = "cour.ttf";
    else if (name.Contains("Consolas", StringComparison.CurrentCultureIgnoreCase)) fontName = "consola.ttf";
    if (!fonts.TryGetValue(fontName, out var font)) {
        font = PdfFontFactory.CreateFont($@"C:\Windows\Fonts\{fontName}", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
        fonts.Add(fontName, font);
    }
    //Console.Write($"{name} -> {fontName}");
    return font;
}

注意:每个PDF需要创建自己的字体实例,不然保存的时候会有异常,引用的资源属于别的文件。

  1. 渲染过程
    只有下列三种渲染的方式。
for (int i = 1; i < docSource.GetNumberOfPages(); i++) {
    info.Clear();
    var page = docSource.GetPage(i);
    //处理原始文件的每一页
    processor.ProcessPageContent(page);
    //根据List<ObjectRenderInfo>内容进行重新绘制
    foreach (var objRenderInfo in info) {
    }
}

4.1 渲染文字

如果需要擦除的水印文字就在这里就很方便的通过判断即可。

var font = GetFont(objRenderInfo.Font.GetFontProgram().GetFontNames().GetFontName());
var paragraph = new Paragraph(objRenderInfo.Text).SetFixedPosition(i, x, y, objRenderInfo.Width * 2)
    .SetFont(font).SetFontSize(fontSize).SetFontColor(objRenderInfo.Color);
docTarget.Add(paragraph);

本过程的灵魂所在就是SetFixedPosition(int pageNumber, float left, float bottom, float width)方法,比对图形处理来说会简单一些,直接对pageNumber指定的页进行绘制文本操作即可。注意width所指的参数这里使用了objRenderInfo.Width * 2,试验过仅用Width可能会导致文本折行,简单起见给定了一个经验值。

4.2 渲染图形

绘制图形会比较多的坑。需要注意的几个点如下:

  • PdfPage的获取:Path的绘制需要PdfCanvas,而后者需要从PdfPage创建,显而易见的想从docTarge.GetPage(i)获取页面实例,可惜想得太天真了。
PdfPage? page = null;
try { page = docTarget.GetPage(i); } catch (Exception) { page = docTarget.AddNewPage(); }
  • Matrix转换矩阵的使用:如果简单的使用PathRenderInfo的几个参数进来不足以绘制和原先一样的图形,是因为有偏移和缩放。
var offset = new Point(objRenderInfo.Matrix.Get(6), objRenderInfo.Matrix.Get(7));
float scaleX = objRenderInfo.Matrix.Get(0), scaleY = objRenderInfo.Matrix.Get(4);
(globalOffset, globalScaleX, globalScaleY) = (offset, scaleX, scaleY);
  • globalOffset, globalScaleX, globalScaleY:单独需要将这几个值保存下来作为本页的全局偏移量以及缩放量,是遇到了一些例如流程图、表格,使用Path绘制的时候PathRenderInfo记载进了Matrix变量。在绘制Shape和上文Text的时候,需要进行计算。
//绘制Text
var x = objRenderInfo.Matrix.Get(6) * globalScaleX + globalOffset.x;
var y = objRenderInfo.Matrix.Get(7) * globalScaleY + globalOffset.y;
var fontSize = (float)(objRenderInfo.FontSize * Math.Sqrt(globalScaleX * globalScaleY));

在这里fontSize做了特殊处理,短时间内还没法知道到底是X还是Y轴需要缩放。

//绘制图形
foreach (var sub in objRenderInfo.Path.GetSubpaths()) {
    canvas.SaveState();
    foreach (var shape in sub.GetSegments()) {
        switch(shape) {
            case iText.Kernel.Geom.Line line:
            //处理直线
            break;
            case iText.Kernel.Geom.BezierCurve curve:
            //处理曲线
            break;
            default: Console.Write(shape); break;
        }
    }
    if (sub.IsClosed()) canvas.ClosePath();
    canvas.Stroke();
    canvas.RestoreState();
}
  • 绘制直线:
var points = line.GetBasePoints();
canvas.MoveTo(offset.x + points[0].x * scaleX, offset.y + points[0].y * scaleY);
canvas.LineTo(offset.x + points[1].x * scaleX, offset.y + points[1].y * scaleY);
  • 绘制曲线:我遇到的这个文件里面是3个点确定一个曲线,理论上按照文档也会有2个点。以下省略的点个数判断。
var points = curve.GetBasePoints();
canvas.MoveTo(offset.x + points[0].x * scaleX, offset.y + points[0].y * scaleY);
canvas.CurveTo(offsetx + points[1].x * scaleX, offset.y + points[1].y * scaleY,
    offsetx + points[2].x * scaleX, offset.y + points[2].y * scaleY,
    offsetx + points[3].x * scaleX, offset.y + points[3].y * scaleY);

应该还存在更简单的使用Matrix的API可以缩减代码量,不过时间太少没有深入研究

4.3 渲染图像
图像的绘制相对简单,但是还有一些坑没填上。比如获取的ImageBytes展示出来是黑块,在不影响阅读的情况下还没研究修复。由于直接可以绘制在指定页面,所以篇幅会很小。

var image = new Image(ImageDataFactory.Create(objRenderInfo.Image))
    .SetFixedPosition(i, objRenderInfo.Vector.Get(0), objRenderInfo.Vector.Get(1));
if (objRenderInfo.Width > page.GetPageSize().GetWidth())
    image.SetAutoScale(true);
else
    image.SetWidth(objRenderInfo.Width).SetHeight(objRenderInfo.Height);
docTarget.Add(image);
  1. 后话
    ObjectRenderInfo的定义
class ObjectRenderInfo {
    public string? Text { get; set; }
    public PdfFont? FontName { get; set; }
    public float FontSize { get; set; }
    public float Width { get; set; }
    public float Height { get; set; }
    public Color? Color { get; set; }
    public Color? Background { get; set; }
    public Matrix? Matrix { get; set; }
    public byte[]? Image { get; set; }
    public Vector? Vector { get; set; }
    public iText.Kernel.Geom.Path? Path { get; set; }
    public int Operation { get; set; }
}

使用上述代码的话,几乎可以将原先PDF内容绘制到新的文件,不过还存在两个问题。

  • 一些图形中带文本的位置会乱。目前尚未找到解决方法。
  • 一些图像展示不出来,仅是一个黑块,因为没有分析二进制图像内存所以还未找到解决方法。
posted @ 2024-09-29 22:40 charset 阅读(133) 评论(0) 推荐(0) 编辑
摘要: 我是CHARSET,转载请保留全文本。 建立POCO public class Eventing : EventArgs { public DateTime EmitTime { get; set; } public string? Message { get; set; } } public cl 阅读全文
posted @ 2022-11-25 16:38 charset 阅读(202) 评论(0) 推荐(0) 编辑
摘要: 在Blazor实现微信小程序扫码登录 ——使用极简登录模型 最近需要开发一个Blazor Server Side页面,需要用到登录功能,作为某微信小程序的后管。在网上搜了一遍,似乎没有找到合适的,所以就自己造了个轮子。几乎都是代码,从来不需要写注释的我。 本文示例后端代码在.NET 6,下用Mini 阅读全文
posted @ 2022-03-17 21:13 charset 阅读(2125) 评论(1) 推荐(0) 编辑
摘要: 编者按:陆陆续续花了比预估时间长的一个《科技管理系统移动审批》需求。因为是第一个吃螃蟹的需求,之前组织里面没有人尝试过使用企业微信对外网的应用,所以走了不少弯路。在同事和搜索引擎的帮助下克服了很多困难,即使是腾讯文档或者外部服务都没法解决。现将一些地雷和排雷方案列举以下以飨读者。 注:企业微信服务器 阅读全文
posted @ 2021-07-26 14:24 charset 阅读(172) 评论(0) 推荐(0) 编辑
摘要: Blazor 极简登录模型 (适用Server Side和WASM Client) 不少介绍Blazor网站包括微软自己的文档网站,对Blazor采用的认证/授权机制有详细的介绍,但是往往给出的是Identity Server的例子。搜索引擎可以找到的如: https://chrissainty.c 阅读全文
posted @ 2021-02-02 15:02 charset 阅读(9883) 评论(9) 推荐(3) 编辑
摘要: 本文不讨论WS性能问题,也不讨论使用非框架方式比如TCP伪造HTTP协议等方式通讯,仅讨论在常见的系统间使用HTTP+SOAP通讯框架的方法以及雷区。 本文大多数内容来自于某次在两个月内迁移完两个异构ESB产品后的思考。 Java Java下框架比较多,常见问题是一种获取(HTTP GET) WSD 阅读全文
posted @ 2016-06-14 10:59 charset 阅读(411) 评论(0) 推荐(0) 编辑
摘要: C# EasyORM 阅读全文
posted @ 2015-06-08 16:40 charset 阅读(330) 评论(0) 推荐(0) 编辑
摘要: Dragable Rectangle with Text Label in Raphaël 阅读全文
posted @ 2013-02-19 11:23 charset 阅读(372) 评论(0) 推荐(0) 编辑
点击右上角即可分享
微信分享提示