I have a pdf which comprises of some data, followed by some whitespace. I don't know how large the data is, but I'd like to trim off the whitespace following the data
PdfReader reader = new PdfReader(PDFLOCATION);
Rectangle rect = new Rectangle(700, 2000);
Document document = new Document(rect);
PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(SAVELCATION));
document.open();
int n = reader.getNumberOfPages();
PdfImportedPage page;
for (int i = 1; i <= n; i++) {
document.newPage();
page = writer.getImportedPage(reader, i);
Image instance = Image.getInstance(page);
document.add(instance);
}
document.close();
Is there a way to clip/trim the whitespace for each page in the new document?
This PDF contains vector graphics.
I'm usung iTextPDF, but can switch to any Java library (mavenized, Apache license preferred)
As no actual solution has been posted, here some pointers from the accompanying itext-questions mailing list thread:
As you want to merely trim pages, this is not a case of PdfWriter + getImportedPage usage but instead of PdfStamper usage. Your main code using a PdfStamper might look like this:
PdfReader reader = new PdfReader(resourceStream);
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream("target/test-outputs/test-trimmed-stamper.pdf"));
// Go through all pages
int n = reader.getNumberOfPages();
for (int i = 1; i <= n; i++)
{
Rectangle pageSize = reader.getPageSize(i);
Rectangle rect = getOutputPageSize(pageSize, reader, i);
PdfDictionary page = reader.getPageN(i);
page.put(PdfName.CROPBOX, new PdfArray(new float[]{rect.getLeft(), rect.getBottom(), rect.getRight(), rect.getTop()}));
stamper.markUsed(page);
}
stamper.close();
As you see I also added another argument to your getOutputPageSize method to-be. It is the page number. The amount of white space to trim might differ on different pages after all.
If the source document did not contain vector graphics, you could simply use the iText parser package classes. There even already is a TextMarginFinder based on them. In this case the getOutputPageSize method (with the additional page parameter) could look like this:
private Rectangle getOutputPageSize(Rectangle pageSize, PdfReader reader, int page) throws IOException
{
PdfReaderContentParser parser = new PdfReaderContentParser(reader);
TextMarginFinder finder = parser.processContent(page, new TextMarginFinder());
Rectangle result = new Rectangle(finder.getLlx(), finder.getLly(), finder.getUrx(), finder.getUry());
System.out.printf("Text/bitmap boundary: %f,%f to %f, %f\n", finder.getLlx(), finder.getLly(), finder.getUrx(), finder.getUry());
return result;
}
Using this method with your file test.pdf results in:
As you see the code trims according to text (and bitmap image) content on the page.
To find the bounding box respecting vector graphics, too, you essentially have to do the same but you have to extend the parser framework used here to inform its listeners (the TextMarginFinder essentially is a listener to drawing events sent from the parser framework) about vector graphics operations, too. This is non-trivial, especially if you don't know PDF syntax by heart yet.
If your PDFs to trim are not too generic but can be forced to include some text or bitmap graphics in relevant positions, though, you could use the sample code above (probably with minor changes) anyways.
E.g. if your PDFs always start with text on top and end with text at the bottom, you could change getOutputPageSize to create the result rectangle like this:
Rectangle result = new Rectangle(pageSize.getLeft(), finder.getLly(), pageSize.getRight(), finder.getUry());
This only trims top and bottom empty space:
Depending on your input data pool and requirements this might suffice.
Or you can use some other heuristics depending on your knowledge on the input data. If you know something about the positioning of text (e.g. the heading to always be centered and some other text to always start at the left), you can easily extend the TextMarginFinder to take advantage of this knowledge.
Recent (April 2015, iText 5.5.6-SNAPSHOT) improvements
The current development version, 5.5.6-SNAPSHOT, extends the parser package to also include vector graphics parsing. This allows for an extension of iText's original TextMarginFinder class implementing the new ExtRenderListener methods like this:
#Override
public void modifyPath(PathConstructionRenderInfo renderInfo)
{
List<Vector> points = new ArrayList<Vector>();
if (renderInfo.getOperation() == PathConstructionRenderInfo.RECT)
{
float x = renderInfo.getSegmentData().get(0);
float y = renderInfo.getSegmentData().get(1);
float w = renderInfo.getSegmentData().get(2);
float h = renderInfo.getSegmentData().get(3);
points.add(new Vector(x, y, 1));
points.add(new Vector(x+w, y, 1));
points.add(new Vector(x, y+h, 1));
points.add(new Vector(x+w, y+h, 1));
}
else if (renderInfo.getSegmentData() != null)
{
for (int i = 0; i < renderInfo.getSegmentData().size()-1; i+=2)
{
points.add(new Vector(renderInfo.getSegmentData().get(i), renderInfo.getSegmentData().get(i+1), 1));
}
}
for (Vector point: points)
{
point = point.cross(renderInfo.getCtm());
Rectangle2D.Float pointRectangle = new Rectangle2D.Float(point.get(Vector.I1), point.get(Vector.I2), 0, 0);
if (currentPathRectangle == null)
currentPathRectangle = pointRectangle;
else
currentPathRectangle.add(pointRectangle);
}
}
#Override
public Path renderPath(PathPaintingRenderInfo renderInfo)
{
if (renderInfo.getOperation() != PathPaintingRenderInfo.NO_OP)
{
if (textRectangle == null)
textRectangle = currentPathRectangle;
else
textRectangle.add(currentPathRectangle);
}
currentPathRectangle = null;
return null;
}
#Override
public void clipPath(int rule)
{
}
(Full source: MarginFinder.java)
Using this class to trim the white space results in
which is pretty much what one would hope for.
Beware: The implementation above is far from optimal. It is not even correct as it includes all curve control points which is too much. Furthermore it ignores stuff like line width or wedge types. It actually merely is a proof-of-concept.
All test code is in TestTrimPdfPage.java.
Related
This is my sample code to draw a box cloud annotation. I used code in PDFBox's implementation to draw a box cloud but i have a little problem when used in iText. I modified the border class and some parts to be usable in iText.
you can find the border class here.
My problem is, the top and right border clouds are not drawn. it seems their location is drawn beyond the rect difference. I figure the issue is with drawing the curves in cloudyPolygonImpl(). maybe itext has different ways to draw in PdfAppearance? I am not sure.
This is the what i have so far.
public class Test {
public static void main(String[] args) throws Exception {
PdfReader reader = new PdfReader("src.pdf");
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream("result.pdf"));
PdfDictionary be = new PdfDictionary();
be.put(PdfName.S, PdfName.C);
be.put(PdfName.I, new PdfNumber(1));
Rectangle location = new Rectangle(123.6f, 584.4f, 252.6f, 653.4f);
PdfAnnotation stamp = PdfAnnotation.createSquareCircle(stamper.getWriter(), location, "", true);
stamp.setBorderStyle(new PdfBorderDictionary(1, PdfBorderDictionary.STYLE_SOLID));
stamp.put(new PdfName("BE"), be);
stamp.setColor(BaseColor.RED);
PdfContentByte cb = stamper.getOverContent(1);
PdfAppearance app = cb.createAppearance(location.getWidth(), location.getHeight());
stamp.setAppearance(PdfName.N, app);
PdfArray stickyRect = stamp.getAsArray(PdfName.RECT);
Rectangle annotRect = new Rectangle(stickyRect.getAsNumber(0).floatValue(),
stickyRect.getAsNumber(1).floatValue(),
stickyRect.getAsNumber(2).floatValue(),
stickyRect.getAsNumber(3).floatValue());
PdfArray arrDiff = annotation.getAsArray(PdfName.RD);
Rectangle annotRectDiff = null;
if (arrDiff != null) {
annotRectDiff = new Rectangle(arrDiff.getAsNumber(0).floatValue(), arrDiff.getAsNumber(1).floatValue(),
arrDiff.getAsNumber(2).floatValue(), arrDiff.getAsNumber(3).floatValue()
}
// Create cloud appearance
CBorder cborder = new CBorder(app, 1, 1, annotRect);
cborder.createCloudyRectangle(annotRectDiff);
stamp.put(PdfName.RECT, new PdfRectangle(cborder.getRectangle()));
stamp.put(PdfName.RD, new PdfArray(new float[] {
cborder.getRectDifference().getLeft(),
cborder.getRectDifference().getBottom(),
cborder.getRectDifference().getRight(),
cborder.getRectDifference().getTop() }));
app.rectangle(cborder.getBBox());
app.transform(cborder.getMatrix());
app.setColorStroke(BaseColor.RED);
app.setLineWidth(1);
app.stroke();
stamper.addAnnotation(stamp, 1);
stamper.close();
reader.close();
}
}
The correct output should be that all borders be drawn with cloud but currently only the left and bottom are drawn.
(This answer is based on the code in revision 3 of your question as the changes in revision 4 introduced multiple errors.)
Your code here creates an invalid annotation appearance stream:
CBorder cborder = new CBorder(app, 1, 1, annotRect);
cborder.createCloudyRectangle(null);
stamp.put(PdfName.RECT, new PdfRectangle(cborder.getRectangle()));
stamp.put(PdfName.RD, new PdfArray(new float[] {
cborder.getRectDifference().getLeft(),
cborder.getRectDifference().getBottom(),
cborder.getRectDifference().getRight(),
cborder.getRectDifference().getTop() }));
app.rectangle(cborder.getBBox());
app.transform(cborder.getMatrix());
app.setColorStroke(BaseColor.RED);
app.setLineWidth(1);
app.stroke();
Its upper part creates a path:
2 j
121.58 588.63 m
122.06 588.95 122.6 589.18 123.16 589.3 c
120.73 588.78 119.18 586.4 119.7 583.96 c
120.19 581.67 122.35 580.14 124.68 580.44 c
...
122.06 596.42 122.6 596.64 123.16 596.76 c
121.09 596.32 119.6 594.49 119.6 592.36 c
119.6 590.87 120.34 589.47 121.58 588.63 c
h
Then app.rectangle(cborder.getBBox()) does not create anything (beware, this rectangle overload does not what you expect it to do!).
Then app.transform(cborder.getMatrix()) adds a change to the current transformation matrix, app.setColorStroke(BaseColor.RED) adds a change of the stroking color, and app.setLineWidth(1) adds a change of the line width:
1 0 0 1 -118.68 -579.48 cm
1 0 0 RG
1 w
And finally app.stroke() adds the command to stroke the path:
S
But between the definition of a path and the corresponding path drawing command, only clipping path instructions are allowed! Cf. Figure 9 – Graphics Objects – in the PDF specification ISO 32000-1.
You can fix this code like this, pulling up color and line width changes, and directly using the cloud bounding box:
// Create cloud appearance
app.setColorStroke(BaseColor.RED);
app.setLineWidth(1);
CBorder cborder = new CBorder(app, 1, 1, annotRect);
cborder.createCloudyRectangle(null);
stamp.put(PdfName.RECT, new PdfRectangle(cborder.getRectangle()));
stamp.put(PdfName.RD, new PdfArray(new float[] {
cborder.getRectDifference().getLeft(),
cborder.getRectDifference().getBottom(),
cborder.getRectDifference().getRight(),
cborder.getRectDifference().getTop() }));
app.stroke();
app.setBoundingBox(cborder.getBBox());
(CloudBoxAnnotation test testDrawLikeChitgoksImproved)
This in particular changes the result (as seen in Adobe Acrobat) from
to
I need to copy annotations using PdfWriter instead of PdfCopy because at the time of the copy I need to resize/rotate the page. Can anyone tell me how to do this?
You think you need to use a plain PdfWriter instead of a PdfCopy for copying PDFs because you need to resize/rotate the page and iText in Action, 2nd Ed, says doing so is not possible with the PdfCopy class. Thus, you look for a way to copy annotations in such a context.
What you should look for instead is a way to rotate or resize pages and at the same time use PdfCopy nonetheless!
While it is true that the PdfCopy class itself does not allow resizing or rotating pages, you can manipulate a PDF loaded into a PdfReader and resize and/or rotate its pages before using the PdfCopy class. If you then copy the pages from this manipulated PdfReader into a PdfCopy, you get a result with resized or rotated pages (due to the manipulated PdfReader) and all the annotations present (due to the use of a PdfCopy).
E.g. you can resize all the pages in a PdfReader like this:
void resize(PdfReader pdfReader, float width, float height) {
for (int i = 1; i <= pdfReader.getNumberOfPages(); i++) {
boolean switched = pdfReader.getPageRotation(i) % 180 != 0;
float widthHere = switched ? height : width;
float heightHere = switched ? width : height;
Rectangle cropBox = pdfReader.getCropBox(i);
float halfWidthGain = (widthHere - cropBox.getWidth()) / 2;
float halfHeightGain = (heightHere - cropBox.getHeight()) / 2;
Rectangle newCropBox = new Rectangle(cropBox.getLeft() - halfWidthGain, cropBox.getBottom() - halfHeightGain,
cropBox.getRight() + halfWidthGain, cropBox.getTop() + halfHeightGain);
Rectangle mediaBox = pdfReader.getPageSize(i);
Rectangle newMediaBox = new Rectangle(Math.min(newCropBox.getLeft(), mediaBox.getLeft()),
Math.min(newCropBox.getBottom(), mediaBox.getBottom()),
Math.max(newCropBox.getRight(), mediaBox.getRight()),
Math.max(newCropBox.getTop(), mediaBox.getTop()));
PdfDictionary pageDictionary = pdfReader.getPageN(i);
pageDictionary.put(PdfName.MEDIABOX, new PdfArray(new float[] {newMediaBox.getLeft(), newMediaBox.getBottom(),
newMediaBox.getRight(), newMediaBox.getTop()}));
pageDictionary.put(PdfName.CROPBOX, new PdfArray(new float[] {newCropBox.getLeft(), newCropBox.getBottom(),
newCropBox.getRight(), newCropBox.getTop()}));
}
}
(CopyWithResizeRotate helper method)
and you can rotate all the pages in a PdfReader like this:
void rotate(PdfReader pdfReader) {
for (int i = 1; i <= pdfReader.getNumberOfPages(); i++) {
int rotation = pdfReader.getPageRotation(i);
int newRotation = rotation + 90 % 360;
PdfDictionary pageDictionary = pdfReader.getPageN(i);
if (newRotation == 0)
pageDictionary.remove(PdfName.ROTATE);
else
pageDictionary.put(PdfName.ROTATE, new PdfNumber(newRotation));
}
}
(CopyWithResizeRotate helper method)
Using these helpers, you can e.g. create a PDF from the rotated and/or resized pages of some source PDF and copy them like this:
byte[] wildPdf = RETRIEVE_SOURCE_PDF;
PdfReader pdfReaderOriginal = new PdfReader(wildPdf);
PdfReader pdfReaderRotate = new PdfReader(wildPdf);
rotate(pdfReaderRotate);
PdfReader pdfReaderResize = new PdfReader(wildPdf);
resize(pdfReaderResize, PageSize.LETTER.getWidth(), PageSize.LETTER.getHeight());
PdfReader pdfReaderRotateResize = new PdfReader(wildPdf);
rotate(pdfReaderRotateResize);
resize(pdfReaderRotateResize, PageSize.LETTER.getWidth(), PageSize.LETTER.getHeight());
try ( OutputStream os = new FileOutputStream(new File(RESULT_FOLDER, "wild-rotated-resized.pdf"))) {
Document document = new Document();
PdfCopy pdfCopy = new PdfCopy(document, os);
document.open();
pdfCopy.addDocument(pdfReaderOriginal);
pdfCopy.addDocument(pdfReaderRotate);
pdfCopy.addDocument(pdfReaderResize);
pdfCopy.addDocument(pdfReaderRotateResize);
document.close();
}
(CopyWithResizeRotate test method testRotateResizeAndCopy)
The result can look as follows, the first row the original pages (#1 A4, #2 HALFLETTER, #3 A5, #4 A5 rotated, #5 500x700), the second row the rotated ones, the third row the resized ones (to LETTER), and the fourth row the rotated and resized ones (to LETTER). The Adobe Reader thumbnails unfortunately are not at all to scale:
Manipulating a single PDF only
If you actually only want to resize/rotate pages of a single input PDF, you should not use a PdfCopy instance but instead a PdfStamper:
PdfReader pdfReader = new PdfReader(SOURCE);
[...manipulate properties of the pdfReader like above...]
new PdfStamper(pdfReader, TARGET_STREAM).close();
The advantage here is that not only page-level data but also document-level data of the original document are retained.
Special annotations
There is one type of annotations which will behave in an unexpected manner with the code above: annotations with the NoRotate flag set. Such annotations will behave like this when their host page is rotated:
(ISO 32000-2 section 12.5.3 — Annotation flags)
I am trying to add a header to existing pdf documents in Java with iText. I can add the header at a fixed place on the document, but all the documents are different page sizes, so it is not always at the top of the page. I have tried getting the page size so that I could calculate the position of the header, but it seems as if the page size is not actually what I want. On some documents, calling reader.getPageSize(i).getTop(20) will place the text in the right place at the top of the page, however, on some different documents it will place it half way down the page. Most of the pages have been scanned be a Xerox copier, if that makes a difference. Here is the code I am using:
PdfReader reader = new PdfReader(readFilePath);
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(writeFilePath));
BaseFont bf = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.CP1252, BaseFont.NOT_EMBEDDED);
for (int i = 1; i <= reader.getNumberOfPages(); i++) {
PdfContentByte cb = stamper.getOverContent(i);
cb.beginText();
cb.setFontAndSize(bf, 14);
float x = reader.getPageSize(i).getWidth() / 2;
float y = reader.getPageSize(i).getTop(20);
cb.showTextAligned(PdfContentByte.ALIGN_CENTER, "Copy", x, y, 0);
cb.endText();
}
stamper.close();
PDF that works correctly
PDF that works incorrectly
Take a look at the StampHeader1 example. I adapted your code, introducing ColumnText.showTextAligned() and using a Phrase for the sake of simplicity (maybe you can change that part of your code too):
public void manipulatePdf(String src, String dest) throws IOException, DocumentException {
PdfReader reader = new PdfReader(src);
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(dest));
Phrase header = new Phrase("Copy", new Font(FontFamily.HELVETICA, 14));
for (int i = 1; i <= reader.getNumberOfPages(); i++) {
float x = reader.getPageSize(i).getWidth() / 2;
float y = reader.getPageSize(i).getTop(20);
ColumnText.showTextAligned(
stamper.getOverContent(i), Element.ALIGN_CENTER,
header, x, y, 0);
}
stamper.close();
reader.close();
}
As you have found out, this code assumes that no rotation was defined.
Now take a look at the StampHeader2 example. I'm using your "Wrong" file and I've added one extra line:
stamper.setRotateContents(false);
By telling the stamper not to rotate the content I'm adding, I'm adding the content using the coordinates as if the page isn't rotated. Please take a look at the result: stamped_header2.pdf. We added "Copy" at the top of the page, but as the page is rotated, we see the word appear on the side. The word is rotated because the page is rotated.
Maybe that's what you want, maybe it isn't. If it isn't, please take a look at StampHeader3 in which I calculate x and y differently, based on the rotation of the page:
if (reader.getPageRotation(i) % 180 == 0) {
x = reader.getPageSize(i).getWidth() / 2;
y = reader.getPageSize(i).getTop(20);
}
else {
x = reader.getPageSize(i).getHeight() / 2;
y = reader.getPageSize(i).getRight(20);
}
Now the word "Copy" appears on what is perceived as the "top of the page" (but in reality, it could be the side of the page): stamped_header3.pdf
I am new to iText and I looked at its many examples. The thing I have hard time to figure it out is the rectangle. On the page
http://developers.itextpdf.com/examples/form-examples-itext5/multiline-fields
there are many examples with hard-coded values for Rectangle objects. For example:
Rectangle rect = new Rectangle(36, 770, 144, 806);
My problem is that I create one Paragraph and I would like to add a fillable text input box (multi-lines) beneath it. How do I know the exact values for creating a Rectangle object that can be nicely put just after the paragraph. The size of the text of a Paragraph can change. So I cannot assume any hard-coded value.
In iText 5, iText keeps track of the coordinates of the content when using document.add(). You could take control yourself by adding content at absolute positions (e.g. by using ColumnText), but that's hard, because then you have to keep track of many things yourself (for instance: you have to introduce page breaks yourself when the content reaches the bottom of the page).
If you leave the control of the coordinates to iText, you can get access to these coordinates by using page events.
Take a look at the example below, where we keep track of the start and the end of a Paragraph in the onParagraph() and onParagraphEnd() method. This code sample is not easy to understand, but it's the only way to get the coordinates of a Paragraph in iText 5 for instance if we want to draw a rectangle around a block of text. As you can read at the bottom of that page, iText 7 makes it much easier to meet this requirement.
If you stick to iText 5, it's much easier to use generic tags to define locations. See the GenericFields example, where we use empty Chunks that result in fields. If you want to see a screen shot of the result, see Add PdfPCell to Paragraph
In your case, I'd create a Paragraph containing a Chunk that spans different lines, and I'd add the field in the onGenericTag() method of a page event.
Suppose that we have the following text file: jekyll_hyde.txt
How do we convert it to a PDF that looks like this:
Note the blue border that is added to the titles, and the page number at the bottom of each page. In iText 5, these elements are added using page events:
class MyPageEvents extends PdfPageEventHelper {
protected float startpos = -1;
protected boolean title = true;
public void setTitle(boolean title) {
this.title = title;
}
#Override
public void onEndPage(PdfWriter writer, Document document) {
Rectangle pagesize = document.getPageSize();
ColumnText.showTextAligned(
writer.getDirectContent(),
Element.ALIGN_CENTER,
new Phrase(String.valueOf(writer.getPageNumber())),
(pagesize.getLeft() + pagesize.getRight()) / 2,
pagesize.getBottom() + 15,
0);
if (startpos != -1)
onParagraphEnd(writer, document,
pagesize.getBottom(document.bottomMargin()));
startpos = pagesize.getTop(document.topMargin());
}
#Override
public void onParagraph(PdfWriter writer, Document document,
float paragraphPosition) {
startpos = paragraphPosition;
}
#Override
public void onParagraphEnd(PdfWriter writer, Document document,
float paragraphPosition) {
if (!title) return;
PdfContentByte canvas = writer.getDirectContentUnder();
Rectangle pagesize = document.getPageSize();
canvas.saveState();
canvas.setColorStroke(BaseColor.BLUE);
canvas.rectangle(
pagesize.getLeft(document.leftMargin()),
paragraphPosition - 3,
pagesize.getWidth() - document.leftMargin() - document.rightMargin(),
startpos - paragraphPosition);
canvas.stroke();
canvas.restoreState();
}
}
We can use the following code to convert a text file to a PDF and introduce the page event to the PdfWriter:
public void createPdf(String dest)
throws DocumentException, IOException {
Document document = new Document();
PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(dest));
MyPageEvents events = new MyPageEvents();
writer.setPageEvent(events);
document.open();
BufferedReader br = new BufferedReader(new FileReader(TEXT));
String line;
Paragraph p;
Font normal = new Font(FontFamily.TIMES_ROMAN, 12);
Font bold = new Font(FontFamily.TIMES_ROMAN, 12, Font.BOLD);
boolean title = true;
while ((line = br.readLine()) != null) {
p = new Paragraph(line, title ? bold : normal);
p.setAlignment(Element.ALIGN_JUSTIFIED);
events.setTitle(title);
document.add(p);
title = line.isEmpty();
}
document.close();
}
Source: developers.itextpdf.com
Used itextpdf-5.5.9 and itext-xtra-5.5.9
I am trying to applying redaction on Partly text string but after applied redaction whole string is removed from document.Please find attached screenshot.
PdfReader reader = new PdfReader(src);
PdfCleanUpProcessor cleaner= null;
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(targetPdf));
stamper.setRotateContents(false);
List<PdfCleanUpLocation> cleanUpLocations = new ArrayList<PdfCleanUpLocation>();
Rectangle rectangle = new Rectangle(380, 640, 430, 665);
cleanUpLocations.add(new PdfCleanUpLocation(1, rectangle, BaseColor.BLACK));
cleaner = new PdfCleanUpProcessor(cleanUpLocations, stamper);
cleaner.cleanUp();
stamper.close();
reader.close();
The OP clarified in comments that it is indeed his expectation that redaction only removes text which is completely contained in the redaction area; text, though, whose bounding box is even partially outside that area, is expected to remain.
This expectation may be unwise as far as security of redaction is concerned because this way text a casual redactor does not see anymore due to the colored redaction area may still remain in the PDF content available to text extraction or even simple copy&paste.
If in spite of such reservations one still wants to tweak PdfCleanup to work like expected by the OP, one essentially merely has to change the PdfCleanUpRegionFilter used by the PdfCleanUpProcessor: The filter implementation used by default rejects a glyph (and so marks it for removal) if its bounding box intersects the redaction area. To fulfill the OP's expectations, this behavior has to be replaced by a check whether the bounding box is completely contained in the redaction area.
This sounds simple. Unfortunately it is not as simple as it sounds because the clean-up code is not designed for easy replacement of the region filter implementation, many relevant objects or methods are private or at best package protected.
Thus, to achieve the OP's desired behavior I simply copied all of the classes from the com.itextpdf.text.pdf package into an own package, therein added a new filter class derived from my copy of PdfCleanUpRegionFilter with the different text rejection algorithm mentioned above, and then changed the copy of PdfCleanUpProcessor to use this other filter class:
/**
* In contrast to the base class {#link PdfCleanUpRegionFilter}, this filter
* only rejects text <b>completely</b> inside the redaction zone. The original
* also rejects text located merely <b>partially</b> inside the redaction zone.
*/
public class StrictPdfCleanUpRegionFilter extends PdfCleanUpRegionFilter
{
public StrictPdfCleanUpRegionFilter(List<Rectangle> rectangles)
{
super(rectangles);
this.rectangles = rectangles;
}
/**
* Checks if the text is COMPLETELY inside render filter region.
*/
#Override
public boolean allowText(TextRenderInfo renderInfo) {
LineSegment ascent = renderInfo.getAscentLine();
LineSegment descent = renderInfo.getDescentLine();
Point2D[] glyphRect = new Point2D[] {
new Point2D.Float(ascent.getStartPoint().get(0), ascent.getStartPoint().get(1)),
new Point2D.Float(ascent.getEndPoint().get(0), ascent.getEndPoint().get(1)),
new Point2D.Float(descent.getEndPoint().get(0), descent.getEndPoint().get(1)),
new Point2D.Float(descent.getStartPoint().get(0), descent.getStartPoint().get(1)),
};
for (Rectangle rectangle : rectangles)
{
boolean glyphInRectangle = true;
for (Point2D point2d : glyphRect)
{
glyphInRectangle &= rectangle.getLeft() <= point2d.getX();
glyphInRectangle &= point2d.getX() <= rectangle.getRight();
glyphInRectangle &= rectangle.getBottom() <= point2d.getY();
glyphInRectangle &= point2d.getY() <= rectangle.getTop();
}
if (glyphInRectangle)
return false;
}
return true;
}
List<Rectangle> rectangles;
}
(StrictPdfCleanUpRegionFilter)
public class StrictPdfCleanUpProcessor {
...
private PdfCleanUpRegionFilter createFilter(List<PdfCleanUpLocation> cleanUpLocations) {
List<Rectangle> regions = new ArrayList<Rectangle>(cleanUpLocations.size());
for (PdfCleanUpLocation location : cleanUpLocations) {
regions.add(location.getRegion());
}
return new StrictPdfCleanUpRegionFilter(regions);
}
...
}
(StrictPdfCleanUpProcessor, my copy of PdfCleanUpProcessor)
All classes can be found here.
It can be used just like the original cleanup implementation, one merely has to remember using the copied classes, not the original ones:
try ( InputStream resource = getClass().getResourceAsStream("Document.pdf");
OutputStream result = new FileOutputStream(new File(OUTPUTDIR, "Document-redacted-strict.pdf")) )
{
PdfReader reader = new PdfReader(resource);
StrictPdfCleanUpProcessor cleaner= null;
PdfStamper stamper = new PdfStamper(reader, result);
stamper.setRotateContents(false);
List<mkl.testarea.itext5.pdfcleanup.PdfCleanUpLocation> cleanUpLocations = new ArrayList<>();
Rectangle rectangle = new Rectangle(380, 640, 430, 665);
cleanUpLocations.add(new mkl.testarea.itext5.pdfcleanup.PdfCleanUpLocation(1, rectangle, BaseColor.BLACK));
cleaner = new StrictPdfCleanUpProcessor(cleanUpLocations, stamper);
cleaner.cleanUp();
stamper.close();
reader.close();
}
(RedactText test method testRedactStrictForMayankPandey)
The example PDF provided by the OP
After redaction using the original classes
After redaction using the tweaked classes
A sanity check with the tweaked classes
To be sure the tweaked classes still removed any text at all, I enlarged the redaction area so that "heet", the last characters of "Document Submission Sheet", were completely contained in the redaction area:
try ( InputStream resource = getClass().getResourceAsStream("Document.pdf");
OutputStream result = new FileOutputStream(new File(OUTPUTDIR, "Document-redacted-strict-large.pdf")) )
{
PdfReader reader = new PdfReader(resource);
StrictPdfCleanUpProcessor cleaner= null;
PdfStamper stamper = new PdfStamper(reader, result);
stamper.setRotateContents(false);
List<mkl.testarea.itext5.pdfcleanup.PdfCleanUpLocation> cleanUpLocations = new ArrayList<>();
Rectangle rectangle = new Rectangle(380, 640, 430, 680);
cleanUpLocations.add(new mkl.testarea.itext5.pdfcleanup.PdfCleanUpLocation(1, rectangle, BaseColor.BLACK));
cleaner = new StrictPdfCleanUpProcessor(cleanUpLocations, stamper);
cleaner.cleanUp();
stamper.close();
reader.close();
}
(RedactText test method testRedactStrictForMayankPandeyLarge)
And indeed, copy&paste (and other text extraction methods, too) now only render
"Document Submission S".