一種使用iText7渲染引擎去除文字水印方法的過(guò)程記錄
有一種PDF文本,使用旋轉(zhuǎn)過(guò)的字體來(lái)作為水印。文件經(jīng)過(guò)密碼保護(hù),不能通過(guò)編輯的方法去除。
轉(zhuǎn)載請(qǐng)保留這一段文字:charset#cnblogs,謝絕CSDN、知乎之流轉(zhuǎn)載
注意:擁有水印并且編輯密碼包含的PDF文檔可能具有版權(quán)保護(hù),本文僅從技術(shù)角度討論可能性。
正常文件可以被打開(kāi)而且顯示無(wú)誤,使用iText7的渲染引擎來(lái)獲取渲染項(xiàng)目,通過(guò)對(duì)目標(biāo)文本的隱藏來(lái)達(dá)到去除文字水印的目的。
以下列舉了一些使用過(guò)程中的注意點(diǎn)和坑:
- 環(huán)境:Windows 11 Home Edition 23H2
- 機(jī)器:Lenovo L490 i5-8265U@1.6GHz 8C16G
- 軟件:.NET 8.0.400, RoslynPad 19.1
-
引用
itext7, 8.0.5,itext7.bouncy-castle-adapter, 8.0.5,itext7.font-asian, 8.0.5,中間用來(lái)解析加密過(guò)的PDF,最后解析亞洲文字。 -
寫(xiě)一個(gè)
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 };
}
沒(méi)啥好說(shuō)的,注冊(cè)三種渲染事件,并且在事件回調(diào)的時(shí)候通過(guò)info將傳遞的內(nèi)容記錄下來(lái)。
List<ObjectRenderInfo> info = new(256);
var strategy = new TextExtractionStrategy(info);
var processor = new PdfCanvasProcessor(strategy);
- 字體的處理
因?yàn)镻DF的字體直接使用的路不通,所以使用簡(jiǎn)單粗暴的映射本地字體文件的方式進(jìn)行。如果有一些復(fù)式字體不考慮。
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;
}
注意:每個(gè)PDF需要?jiǎng)?chuàng)建自己的字體實(shí)例,不然保存的時(shí)候會(huì)有異常,引用的資源屬于別的文件。
- 渲染過(guò)程
只有下列三種渲染的方式。
for (int i = 1; i < docSource.GetNumberOfPages(); i++) {
info.Clear();
var page = docSource.GetPage(i);
//處理原始文件的每一頁(yè)
processor.ProcessPageContent(page);
//根據(jù)List<ObjectRenderInfo>內(nèi)容進(jìn)行重新繪制
foreach (var objRenderInfo in info) {
}
}
4.1 渲染文字
如果需要擦除的水印文字就在這里就很方便的通過(guò)判斷即可。
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);
本過(guò)程的靈魂所在就是SetFixedPosition(int pageNumber, float left, float bottom, float width)方法,比對(duì)圖形處理來(lái)說(shuō)會(huì)簡(jiǎn)單一些,直接對(duì)pageNumber指定的頁(yè)進(jìn)行繪制文本操作即可。注意width所指的參數(shù)這里使用了objRenderInfo.Width * 2,試驗(yàn)過(guò)僅用Width可能會(huì)導(dǎo)致文本折行,簡(jiǎn)單起見(jiàn)給定了一個(gè)經(jīng)驗(yàn)值。
4.2 渲染圖形
繪制圖形會(huì)比較多的坑。需要注意的幾個(gè)點(diǎn)如下:
PdfPage的獲取:Path的繪制需要PdfCanvas,而后者需要從PdfPage創(chuàng)建,顯而易見(jiàn)的想從docTarge.GetPage(i)獲取頁(yè)面實(shí)例,可惜想得太天真了。
PdfPage? page = null;
try { page = docTarget.GetPage(i); } catch (Exception) { page = docTarget.AddNewPage(); }
Matrix轉(zhuǎn)換矩陣的使用:如果簡(jiǎn)單的使用PathRenderInfo的幾個(gè)參數(shù)進(jìn)來(lái)不足以繪制和原先一樣的圖形,是因?yàn)橛衅坪涂s放。
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:?jiǎn)为?dú)需要將這幾個(gè)值保存下來(lái)作為本頁(yè)的全局偏移量以及縮放量,是遇到了一些例如流程圖、表格,使用Path繪制的時(shí)候PathRenderInfo記載進(jìn)了Matrix變量。在繪制Shape和上文Text的時(shí)候,需要進(jìn)行計(jì)算。
//繪制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做了特殊處理,短時(shí)間內(nèi)還沒(méi)法知道到底是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);
- 繪制曲線:我遇到的這個(gè)文件里面是3個(gè)點(diǎn)確定一個(gè)曲線,理論上按照文檔也會(huì)有2個(gè)點(diǎn)。以下省略的點(diǎn)個(gè)數(shù)判斷。
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);
應(yīng)該還存在更簡(jiǎn)單的使用Matrix的API可以縮減代碼量,不過(guò)時(shí)間太少?zèng)]有深入研究
4.3 渲染圖像
圖像的繪制相對(duì)簡(jiǎn)單,但是還有一些坑沒(méi)填上。比如獲取的ImageBytes展示出來(lái)是黑塊,在不影響閱讀的情況下還沒(méi)研究修復(fù)。由于直接可以繪制在指定頁(yè)面,所以篇幅會(huì)很小。
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);
- 后話(huà)
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; }
}
使用上述代碼的話(huà),幾乎可以將原先PDF內(nèi)容繪制到新的文件,不過(guò)還存在兩個(gè)問(wèn)題。
- 一些圖形中帶文本的位置會(huì)亂。目前尚未找到解決方法。
- 一些圖像展示不出來(lái),僅是一個(gè)黑塊,因?yàn)闆](méi)有分析二進(jìn)制圖像內(nèi)存所以還未找到解決方法。

浙公網(wǎng)安備 33010602011771號(hào)