Удаление водяных знаков из PDF iTextSharp
Я прошел через предложенное здесь решение, но моя проблема немного отличается. В решении, представленном по приведенной выше ссылке, водяной знак можно удалить только в том случае, если для добавления водяного знака также используется iTextSharp. В моем случае я добавляю водяной знак в некоторых случаях, используя Microsoft Word. Когда я использую следующий код, водяной знак исчезает из PDF, но когда я конвертирую PDF в Word, он снова появляется в виде изображения. Насколько я понимаю, код ниже делает то, что он изменяет значение непрозрачности водяного знака на 0, и поэтому он исчезает.
private static void removeWatermark(string watermarkedFile, string unwatermarkedFile)
{
PdfReader.unethicalreading = true;
PdfReader reader = new PdfReader(watermarkedFile);
reader.RemoveUnusedObjects();
int pageCount = reader.NumberOfPages;
for (int i = 1; i <= pageCount; i++)
{
var page = reader.GetPageN(i);
PdfDictionary resources = page.GetAsDict(PdfName.RESOURCES);
PdfDictionary extGStates = resources.GetAsDict(PdfName.EXTGSTATE);
if (extGStates == null)
continue;
foreach (PdfName name in extGStates.Keys)
{
var obj = extGStates.Get(name);
PdfDictionary extGStateObject = (PdfDictionary)PdfReader.GetPdfObject(obj);
var stateNumber = extGStateObject.Get(PdfName.ca);
if (stateNumber == null)
continue;
var caNumber = (PdfNumber)PdfReader.GetPdfObject(stateNumber);
if (caNumber.FloatValue != 1f)
{
extGStateObject.Remove(PdfName.ca);
extGStateObject.Put(PdfName.ca, new PdfNumber(0f));
}
}
}
using (FileStream fs = new FileStream(unwatermarkedFile, FileMode.Create, FileAccess.Write, FileShare.None))
{
using (PdfStamper stamper = new PdfStamper(reader, fs))
{
stamper.SetFullCompression();
stamper.Close();
}
}
}
Есть ли способ удалить этот водяной знак, изменив код?
1 ответ
Как уже упоминалось в OP, если у вас есть полный контроль над процессом создания водяного знака, вы можете поступить так, как объяснил @ChrisHaas в своем ответе на вопрос, на который ссылался OP.
Если, с другой стороны, инструмент, с помощью которого вы создаете водяной знак, делает это по-своему, вам потребуется метод, настроенный для этих водяных знаков.
Этот метод обычно требует, чтобы вы отредактировали некоторый поток контента. Кстати, решение @ChrisHaas тоже так делает.
Чтобы сделать это проще, следует начать с создания общей функции редактирования потока контента, а затем использовать эту функцию только для редактирования этих водяных знаков.
Таким образом, здесь сначала образец классового редактора потока контента, а затем решение на его основе для редактирования образца водяного знака OP.
Универсальный класс редактора потока контента
это PdfContentStreamEditor
класс анализирует исходную инструкцию потока контента, отслеживая инструкции части графического состояния; инструкции передаются на его Write
метод, который по умолчанию записывает их обратно, как только они приходят, эффективно создавая идентичную или, по крайней мере, эквивалентную копию исходного потока.
Чтобы на самом деле редактировать поток, просто переопределите это Write
метод и только направить нужные инструкции в потоке результатов на базу Write
метод.
public class PdfContentStreamEditor : PdfContentStreamProcessor
{
/**
* This method edits the immediate contents of a page, i.e. its content stream.
* It explicitly does not descent into form xobjects, patterns, or annotations.
*/
public void EditPage(PdfStamper pdfStamper, int pageNum)
{
PdfReader pdfReader = pdfStamper.Reader;
PdfDictionary page = pdfReader.GetPageN(pageNum);
byte[] pageContentInput = ContentByteUtils.GetContentBytesForPage(pdfReader, pageNum);
page.Remove(PdfName.CONTENTS);
EditContent(pageContentInput, page.GetAsDict(PdfName.RESOURCES), pdfStamper.GetUnderContent(pageNum));
}
/**
* This method processes the content bytes and outputs to the given canvas.
* It explicitly does not descent into form xobjects, patterns, or annotations.
*/
public void EditContent(byte[] contentBytes, PdfDictionary resources, PdfContentByte canvas)
{
this.canvas = canvas;
ProcessContent(contentBytes, resources);
this.canvas = null;
}
/**
* This method writes content stream operations to the target canvas. The default
* implementation writes them as they come, so it essentially generates identical
* copies of the original instructions the {@link ContentOperatorWrapper} instances
* forward to it.
*
* Override this method to achieve some fancy editing effect.
*/
protected virtual void Write(PdfContentStreamProcessor processor, PdfLiteral operatorLit, List<PdfObject> operands)
{
int index = 0;
foreach (PdfObject pdfObject in operands)
{
pdfObject.ToPdf(canvas.PdfWriter, canvas.InternalBuffer);
canvas.InternalBuffer.Append(operands.Count > ++index ? (byte) ' ' : (byte) '\n');
}
}
//
// constructor giving the parent a dummy listener to talk to
//
public PdfContentStreamEditor() : base(new DummyRenderListener())
{
}
//
// Overrides of PdfContentStreamProcessor methods
//
public override IContentOperator RegisterContentOperator(String operatorString, IContentOperator newOperator)
{
ContentOperatorWrapper wrapper = new ContentOperatorWrapper();
wrapper.setOriginalOperator(newOperator);
IContentOperator formerOperator = base.RegisterContentOperator(operatorString, wrapper);
return formerOperator is ContentOperatorWrapper ? ((ContentOperatorWrapper)formerOperator).getOriginalOperator() : formerOperator;
}
public override void ProcessContent(byte[] contentBytes, PdfDictionary resources)
{
this.resources = resources;
base.ProcessContent(contentBytes, resources);
this.resources = null;
}
//
// members holding the output canvas and the resources
//
protected PdfContentByte canvas = null;
protected PdfDictionary resources = null;
//
// A content operator class to wrap all content operators to forward the invocation to the editor
//
class ContentOperatorWrapper : IContentOperator
{
public IContentOperator getOriginalOperator()
{
return originalOperator;
}
public void setOriginalOperator(IContentOperator originalOperator)
{
this.originalOperator = originalOperator;
}
public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List<PdfObject> operands)
{
if (originalOperator != null && !"Do".Equals(oper.ToString()))
{
originalOperator.Invoke(processor, oper, operands);
}
((PdfContentStreamEditor)processor).Write(processor, oper, operands);
}
private IContentOperator originalOperator = null;
}
//
// A dummy render listener to give to the underlying content stream processor to feed events to
//
class DummyRenderListener : IRenderListener
{
public void BeginTextBlock() { }
public void RenderText(TextRenderInfo renderInfo) { }
public void EndTextBlock() { }
public void RenderImage(ImageRenderInfo renderInfo) { }
}
}
Некоторые фоны:
Этот класс расширяет PdfContentStreamProcessor
из пространства имен парсера iTextSharp. Этот класс изначально предназначен для простого анализа потоков содержимого, чтобы получить информацию для извлечения текста, изображений или графики. Мы используем его для отслеживания части графического состояния, точнее тех параметров графического состояния, которые важны для извлечения текста.
Если для конкретных задач редактирования также требуется предварительно обработанная информация, например, о тексте, нарисованном текущей инструкцией, можно использовать пользовательский IRenderListener
реализация для получения этой информации вместо DummyRenderListener
используется здесь, который просто игнорирует это.
Эта классовая архитектура вдохновлена PdfCleanUpProcessor
из дополнительной библиотеки iTextSharp.xtra.
Редактор, чтобы скрыть водяной знак ОП
Как ОП уже выяснил, его водяные знаки могут быть распознаны как единственные части документа, использующие прозрачность, определенную в объекте ExtGState как значение ca. Поэтому, чтобы скрыть водяной знак, мы должны
- распознавать изменения графического состояния относительно этого значения и
- ничего не рисовать, когда распознанное текущее значение ca меньше 1.
На самом деле водяной знак строится с использованием операций векторной графики. Таким образом, мы можем ограничить наше редактирование этими операциями. Мы можем даже ограничить его, чтобы изменить окончательную инструкцию рисования ("обводка" / "заливка" / "заливка и обводка" плюс некоторые варианты), чтобы не выполнять ту часть (заполнение или обводка), которая генерирует прозрачное содержимое.
public class TransparentGraphicsRemover : PdfContentStreamEditor
{
protected override void Write(PdfContentStreamProcessor processor, PdfLiteral oper, List<PdfObject> operands)
{
String operatorString = oper.ToString();
if ("gs".Equals(operatorString))
{
updateTransparencyFrom((PdfName) operands[0]);
}
if (operatorMapping.Keys.Contains(operatorString))
{
// Downgrade the drawing operator if transparency is involved
// For details cf. the comment before the operatorMapping declaration
PdfLiteral[] mapping = operatorMapping[operatorString];
int index = 0;
if (strokingAlpha < 1)
index |= 1;
if (nonStrokingAlpha < 1)
index |= 2;
oper = mapping[index];
operands[operands.Count - 1] = oper;
}
base.Write(processor, oper, operands);
}
// The current transparency values; beware: save and restore state operations are ignored!
float strokingAlpha = 1;
float nonStrokingAlpha = 1;
void updateTransparencyFrom(PdfName gsName)
{
PdfDictionary extGState = getGraphicsStateDictionary(gsName);
if (extGState != null)
{
PdfNumber number = extGState.GetAsNumber(PdfName.ca);
if (number != null)
nonStrokingAlpha = number.FloatValue;
number = extGState.GetAsNumber(PdfName.CA);
if (number != null)
strokingAlpha = number.FloatValue;
}
}
PdfDictionary getGraphicsStateDictionary(PdfName gsName)
{
PdfDictionary extGStates = resources.GetAsDict(PdfName.EXTGSTATE);
return extGStates.GetAsDict(gsName);
}
//
// Map from an operator name to an array of operations it becomes depending
// on the current graphics state:
//
// * [0] the operation in case of no transparency
// * [1] the operation in case of stroking transparency
// * [2] the operation in case of non-stroking transparency
// * [3] the operation in case of stroking and non-stroking transparency
//
Dictionary<String, PdfLiteral[]> operatorMapping = new Dictionary<String, PdfLiteral[]>();
public TransparentGraphicsRemover()
{
PdfLiteral _S = new PdfLiteral("S");
PdfLiteral _s = new PdfLiteral("s");
PdfLiteral _f = new PdfLiteral("f");
PdfLiteral _fStar = new PdfLiteral("f*");
PdfLiteral _B = new PdfLiteral("B");
PdfLiteral _BStar = new PdfLiteral("B*");
PdfLiteral _b = new PdfLiteral("b");
PdfLiteral _bStar = new PdfLiteral("b*");
PdfLiteral _n = new PdfLiteral("n");
operatorMapping["S"] = new PdfLiteral[]{ _S, _n, _S, _n };
operatorMapping["s"] = new PdfLiteral[]{ _s, _n, _s, _n };
operatorMapping["f"] = new PdfLiteral[]{ _f, _f, _n, _n };
operatorMapping["F"] = new PdfLiteral[]{ _f, _f, _n, _n };
operatorMapping["f*"] = new PdfLiteral[]{ _fStar, _fStar, _n, _n };
operatorMapping["B"] = new PdfLiteral[]{ _B, _f, _S, _n };
operatorMapping["B*"] = new PdfLiteral[]{ _BStar, _fStar, _S, _n };
operatorMapping["b"] = new PdfLiteral[] { _b, _f, _s, _n };
operatorMapping["b*"] = new PdfLiteral[]{ _bStar, _fStar, _s, _n };
}
}
Осторожно: этот редактор примеров очень прост:
- Он учитывает только прозрачность, созданную параметрами ExtGState ca и CA, в частности он игнорирует маски.
- Он не ищет операции сохранения или восстановления графического состояния.
Эти ограничения могут быть легко сняты, но требуют больше кода, чем необходимо для ответа на стеке.
Применение этого редактора к образцу файла OP следующим образом
string source = @"test3.pdf";
string dest = @"test3-noTransparency.pdf";
using (PdfReader pdfReader = new PdfReader(source))
using (PdfStamper pdfStamper = new PdfStamper(pdfReader, new FileStream(dest, FileMode.Create, FileAccess.Write)))
{
PdfContentStreamEditor editor = new TransparentGraphicsRemover();
for (int i = 1; i <= pdfReader.NumberOfPages; i++)
{
editor.EditPage(pdfStamper, i);
}
}
приводит к файлу PDF без водяного знака.
У меня нет инструментов, с помощью которых OP экспортировал содержимое в word, NitroPDF и Foxit, поэтому я не смог выполнить финальный тест. Adobe Acrobat (версия 9.5) по крайней мере при экспорте в Word не содержит водяного знака.
Если инструменты OP по-прежнему имеют следы водяного знака в экспортированных файлах Word, можно легко улучшить этот класс, чтобы фактически исключить создание путей и операции рисования при активной прозрачности.
То же самое в Java
Я начал реализовывать это для iText в Java и только позже понял, что у OP был iTextSharp в.Net. Вот эквивалентные классы Java:
public class PdfContentStreamEditor extends PdfContentStreamProcessor
{
/**
* This method edits the immediate contents of a page, i.e. its content stream.
* It explicitly does not descent into form xobjects, patterns, or annotations.
*/
public void editPage(PdfStamper pdfStamper, int pageNum) throws IOException
{
PdfReader pdfReader = pdfStamper.getReader();
PdfDictionary page = pdfReader.getPageN(pageNum);
byte[] pageContentInput = ContentByteUtils.getContentBytesForPage(pdfReader, pageNum);
page.remove(PdfName.CONTENTS);
editContent(pageContentInput, page.getAsDict(PdfName.RESOURCES), pdfStamper.getUnderContent(pageNum));
}
/**
* This method processes the content bytes and outputs to the given canvas.
* It explicitly does not descent into form xobjects, patterns, or annotations.
*/
public void editContent(byte[] contentBytes, PdfDictionary resources, PdfContentByte canvas)
{
this.canvas = canvas;
processContent(contentBytes, resources);
this.canvas = null;
}
/**
* <p>
* This method writes content stream operations to the target canvas. The default
* implementation writes them as they come, so it essentially generates identical
* copies of the original instructions the {@link ContentOperatorWrapper} instances
* forward to it.
* </p>
* <p>
* Override this method to achieve some fancy editing effect.
* </p>
*/
protected void write(PdfContentStreamProcessor processor, PdfLiteral operator, List<PdfObject> operands) throws IOException
{
int index = 0;
for (PdfObject object : operands)
{
object.toPdf(canvas.getPdfWriter(), canvas.getInternalBuffer());
canvas.getInternalBuffer().append(operands.size() > ++index ? (byte) ' ' : (byte) '\n');
}
}
//
// constructor giving the parent a dummy listener to talk to
//
public PdfContentStreamEditor()
{
super(new DummyRenderListener());
}
//
// Overrides of PdfContentStreamProcessor methods
//
@Override
public ContentOperator registerContentOperator(String operatorString, ContentOperator operator)
{
ContentOperatorWrapper wrapper = new ContentOperatorWrapper();
wrapper.setOriginalOperator(operator);
ContentOperator formerOperator = super.registerContentOperator(operatorString, wrapper);
return formerOperator instanceof ContentOperatorWrapper ? ((ContentOperatorWrapper)formerOperator).getOriginalOperator() : formerOperator;
}
@Override
public void processContent(byte[] contentBytes, PdfDictionary resources)
{
this.resources = resources;
super.processContent(contentBytes, resources);
this.resources = null;
}
//
// members holding the output canvas and the resources
//
protected PdfContentByte canvas = null;
protected PdfDictionary resources = null;
//
// A content operator class to wrap all content operators to forward the invocation to the editor
//
class ContentOperatorWrapper implements ContentOperator
{
public ContentOperator getOriginalOperator()
{
return originalOperator;
}
public void setOriginalOperator(ContentOperator originalOperator)
{
this.originalOperator = originalOperator;
}
@Override
public void invoke(PdfContentStreamProcessor processor, PdfLiteral operator, ArrayList<PdfObject> operands) throws Exception
{
if (originalOperator != null && !"Do".equals(operator.toString()))
{
originalOperator.invoke(processor, operator, operands);
}
write(processor, operator, operands);
}
private ContentOperator originalOperator = null;
}
//
// A dummy render listener to give to the underlying content stream processor to feed events to
//
static class DummyRenderListener implements RenderListener
{
@Override
public void beginTextBlock() { }
@Override
public void renderText(TextRenderInfo renderInfo) { }
@Override
public void endTextBlock() { }
@Override
public void renderImage(ImageRenderInfo renderInfo) { }
}
}
( PdfContentStreamEditor.java)
public class TransparentGraphicsRemover extends PdfContentStreamEditor
{
@Override
protected void write(PdfContentStreamProcessor processor, PdfLiteral operator, List<PdfObject> operands) throws IOException
{
String operatorString = operator.toString();
if ("gs".equals(operatorString))
{
updateTransparencyFrom((PdfName) operands.get(0));
}
PdfLiteral[] mapping = operatorMapping.get(operatorString);
if (mapping != null)
{
int index = 0;
if (strokingAlpha < 1)
index |= 1;
if (nonStrokingAlpha < 1)
index |= 2;
operator = mapping[index];
operands.set(operands.size() - 1, operator);
}
super.write(processor, operator, operands);
}
// The current transparency values; beware: save and restore state operations are ignored!
float strokingAlpha = 1;
float nonStrokingAlpha = 1;
void updateTransparencyFrom(PdfName gsName)
{
PdfDictionary extGState = getGraphicsStateDictionary(gsName);
if (extGState != null)
{
PdfNumber number = extGState.getAsNumber(PdfName.ca);
if (number != null)
nonStrokingAlpha = number.floatValue();
number = extGState.getAsNumber(PdfName.CA);
if (number != null)
strokingAlpha = number.floatValue();
}
}
PdfDictionary getGraphicsStateDictionary(PdfName gsName)
{
PdfDictionary extGStates = resources.getAsDict(PdfName.EXTGSTATE);
return extGStates.getAsDict(gsName);
}
//
// Map from an operator name to an array of operations it becomes depending
// on the current graphics state:
//
// * [0] the operation in case of no transparency
// * [1] the operation in case of stroking transparency
// * [2] the operation in case of non-stroking transparency
// * [3] the operation in case of stroking and non-stroking transparency
//
static Map<String, PdfLiteral[]> operatorMapping = new HashMap<String, PdfLiteral[]>();
static
{
PdfLiteral _S = new PdfLiteral("S");
PdfLiteral _s = new PdfLiteral("s");
PdfLiteral _f = new PdfLiteral("f");
PdfLiteral _fStar = new PdfLiteral("f*");
PdfLiteral _B = new PdfLiteral("B");
PdfLiteral _BStar = new PdfLiteral("B*");
PdfLiteral _b = new PdfLiteral("b");
PdfLiteral _bStar = new PdfLiteral("b*");
PdfLiteral _n = new PdfLiteral("n");
operatorMapping.put("S", new PdfLiteral[]{ _S, _n, _S, _n });
operatorMapping.put("s", new PdfLiteral[]{ _s, _n, _s, _n });
operatorMapping.put("f", new PdfLiteral[]{ _f, _f, _n, _n });
operatorMapping.put("F", new PdfLiteral[]{ _f, _f, _n, _n });
operatorMapping.put("f*", new PdfLiteral[]{ _fStar, _fStar, _n, _n });
operatorMapping.put("B", new PdfLiteral[]{ _B, _f, _S, _n });
operatorMapping.put("B*", new PdfLiteral[]{ _BStar, _fStar, _S, _n });
operatorMapping.put("b", new PdfLiteral[]{ _b, _f, _s, _n });
operatorMapping.put("b*", new PdfLiteral[]{ _bStar, _fStar, _s, _n });
}
}
( TransparentGraphicsRemover.java)
@Test
public void testRemoveTransparentGraphicsTest3() throws IOException, DocumentException
{
try ( InputStream resource = getClass().getResourceAsStream("test3.pdf");
OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "test3-noTransparency.pdf")))
{
PdfReader pdfReader = new PdfReader(resource);
PdfStamper pdfStamper = new PdfStamper(pdfReader, result);
PdfContentStreamEditor editor = new TransparentGraphicsRemover();
for (int i = 1; i <= pdfReader.getNumberOfPages(); i++)
{
editor.editPage(pdfStamper, i);
}
pdfStamper.close();
}
}
(выдержка из EditPageContent.java)
Вот код для изменения цвета (гиперссылки и т. Д.).
PdfCanvasEditor editor = new PdfCanvasEditor() {
@Override
protected void write(PdfCanvasProcessor processor, PdfLiteral operator, List<PdfObject> operands)
{
String operatorString = operator.toString();
if (SET_FILL_RGB.equals(operatorString) && operands.size() == 4) {
if (isApproximatelyEqual(operands.get(0), 0) &&
isApproximatelyEqual(operands.get(1), 0) &&
isApproximatelyEqual(operands.get(2), 1)) {
super.write(processor, new PdfLiteral("g"), Arrays.asList(new PdfNumber(0), new PdfLiteral("g")));
return;
}
}
if (SET_STROKE_RGB.equals(operatorString) && operands.size() == 4) {
if (isApproximatelyEqual(operands.get(0), 0) &&
isApproximatelyEqual(operands.get(1), 0) &&
isApproximatelyEqual(operands.get(2), 1)) {
super.write(processor, new PdfLiteral("G"), Arrays.asList(new PdfNumber(0), new PdfLiteral("G")));
return;
}
}
super.write(processor, operator, operands);
}
boolean isApproximatelyEqual(PdfObject number, float reference) {
return number instanceof PdfNumber && Math.abs(reference - ((PdfNumber)number).floatValue()) < 0.01f;
}
final String SET_FILL_RGB = "rg";
final String SET_STROKE_RGB = "RG";
};
for (int i = 1; i <= pdfDocument.getNumberOfPages(); i++) {
editor.editPage(pdfDocument, i);
}
}