When you want to create a visible signature using PDFBox you need to create a Rectangle2D object.
Rectangle2D humanRect = new Rectangle2D.Float(100, 200, 150, 50);
I would like to know if it is possible to find all the white spaces(rectangles) in the document(or from the first/last page) of a certain size (width x height). I would like to choose one of these positions for my signature form.
I would like to use it as in the following example:
Rectangle2D humanRect = new Rectangle2D.Float(foundX, foundY, width, height);
As already confirmed in a comment to the question, you essentially are looking for a port of the functionality of the FreeSpaceFinder
and FreeSpaceFinderExt
classes for iText from this answer to PDFBox. This is the focus of this answer:
If you want to determine something from the content stream instructions of a page with PDFBox, you usually will create a class based on PDFStreamEngine
or one of its subclasses. For anything that's not focusing on text extraction most often the PDFGraphicsStreamEngine
is the base class of choice.
Based on that we can essentially copy the functionality of the mentioned iText based classes:
public class FreeSpaceFinder extends PDFGraphicsStreamEngine {
//
// constructors
//
public FreeSpaceFinder(PDPage page, float minWidth, float minHeight) {
this(page, page.getCropBox().toGeneralPath().getBounds2D(), minWidth, minHeight);
}
public FreeSpaceFinder(PDPage page, Rectangle2D initialBox, float minWidth, float minHeight) {
this(page, Collections.singleton(initialBox), minWidth, minHeight);
}
public FreeSpaceFinder(PDPage page, Collection<Rectangle2D> initialBoxes, float minWidth, float minHeight) {
super(page);
this.minWidth = minWidth;
this.minHeight = minHeight;
this.freeSpaces = initialBoxes;
}
//
// Result
//
public Collection<Rectangle2D> getFreeSpaces() {
return freeSpaces;
}
//
// Text
//
@Override
protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, Vector displacement)
throws IOException {
super.showGlyph(textRenderingMatrix, font, code, displacement);
Shape shape = calculateGlyphBounds(textRenderingMatrix, font, code);
if (shape != null) {
Rectangle2D rect = shape.getBounds2D();
remove(rect);
}
}
/**
* Copy of <code>org.apache.pdfbox.examples.util.DrawPrintTextLocations.calculateGlyphBounds(Matrix, PDFont, int)</code>.
*/
private Shape calculateGlyphBounds(Matrix textRenderingMatrix, PDFont font, int code) throws IOException
{
GeneralPath path = null;
AffineTransform at = textRenderingMatrix.createAffineTransform();
at.concatenate(font.getFontMatrix().createAffineTransform());
if (font instanceof PDType3Font)
{
// It is difficult to calculate the real individual glyph bounds for type 3 fonts
// because these are not vector fonts, the content stream could contain almost anything
// that is found in page content streams.
PDType3Font t3Font = (PDType3Font) font;
PDType3CharProc charProc = t3Font.getCharProc(code);
if (charProc != null)
{
BoundingBox fontBBox = t3Font.getBoundingBox();
PDRectangle glyphBBox = charProc.getGlyphBBox();
if (glyphBBox != null)
{
// PDFBOX-3850: glyph bbox could be larger than the font bbox
glyphBBox.setLowerLeftX(Math.max(fontBBox.getLowerLeftX(), glyphBBox.getLowerLeftX()));
glyphBBox.setLowerLeftY(Math.max(fontBBox.getLowerLeftY(), glyphBBox.getLowerLeftY()));
glyphBBox.setUpperRightX(Math.min(fontBBox.getUpperRightX(), glyphBBox.getUpperRightX()));
glyphBBox.setUpperRightY(Math.min(fontBBox.getUpperRightY(), glyphBBox.getUpperRightY()));
path = glyphBBox.toGeneralPath();
}
}
}
else if (font instanceof PDVectorFont)
{
PDVectorFont vectorFont = (PDVectorFont) font;
path = vectorFont.getPath(code);
if (font instanceof PDTrueTypeFont)
{
PDTrueTypeFont ttFont = (PDTrueTypeFont) font;
int unitsPerEm = ttFont.getTrueTypeFont().getHeader().getUnitsPerEm();
at.scale(1000d / unitsPerEm, 1000d / unitsPerEm);
}
if (font instanceof PDType0Font)
{
PDType0Font t0font = (PDType0Font) font;
if (t0font.getDescendantFont() instanceof PDCIDFontType2)
{
int unitsPerEm = ((PDCIDFontType2) t0font.getDescendantFont()).getTrueTypeFont().getHeader().getUnitsPerEm();
at.scale(1000d / unitsPerEm, 1000d / unitsPerEm);
}
}
}
else if (font instanceof PDSimpleFont)
{
PDSimpleFont simpleFont = (PDSimpleFont) font;
// these two lines do not always work, e.g. for the TT fonts in file 032431.pdf
// which is why PDVectorFont is tried first.
String name = simpleFont.getEncoding().getName(code);
path = simpleFont.getPath(name);
}
else
{
// shouldn't happen, please open issue in JIRA
System.out.println("Unknown font class: " + font.getClass());
}
if (path == null)
{
return null;
}
return at.createTransformedShape(path.getBounds2D());
}
//
// Bitmaps
//
@Override
public void drawImage(PDImage pdImage) throws IOException {
Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
Rectangle2D unitSquare = new Rectangle2D.Float(0, 0, 1, 1);
Path2D path = new Path2D.Float(unitSquare);
path.transform(ctm.createAffineTransform());
remove(path.getBounds2D());
}
//
// Paths
//
@Override
public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException {
currentPath.moveTo(p0.getX(), p0.getY());
currentPath.lineTo(p1.getX(), p1.getY());
currentPath.lineTo(p2.getX(), p2.getY());
currentPath.lineTo(p3.getX(), p3.getY());
currentPath.closePath();
}
@Override
public void clip(int windingRule) throws IOException {
// ignore
}
@Override
public void moveTo(float x, float y) throws IOException {
currentPath.moveTo(x, y);
}
@Override
public void lineTo(float x, float y) throws IOException {
currentPath.lineTo(x, y);
}
@Override
public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) throws IOException {
currentPath.curveTo(x1, y1, x2, y2, x3, y3);
}
@Override
public Point2D getCurrentPoint() throws IOException {
// To prevent many warnings...
return new Point2D.Float();
}
@Override
public void closePath() throws IOException {
currentPath.closePath();
}
@Override
public void endPath() throws IOException {
currentPath = new Path2D.Float();
}
@Override
public void strokePath() throws IOException {
// Better only remove the bounding boxes of the constituting strokes
remove(currentPath.getBounds2D());
currentPath = new Path2D.Float();
}
@Override
public void fillPath(int windingRule) throws IOException {
// Better only remove the bounding boxes of the constituting subpaths
remove(currentPath.getBounds2D());
currentPath = new Path2D.Float();
}
@Override
public void fillAndStrokePath(int windingRule) throws IOException {
// Better only remove the bounding boxes of the constituting subpaths
remove(currentPath.getBounds2D());
currentPath = new Path2D.Float();
}
@Override
public void shadingFill(COSName shadingName) throws IOException {
// ignore
}
//
// helpers
//
void remove(Rectangle2D usedSpace)
{
final double minX = usedSpace.getMinX();
final double maxX = usedSpace.getMaxX();
final double minY = usedSpace.getMinY();
final double maxY = usedSpace.getMaxY();
final Collection<Rectangle2D> newFreeSpaces = new ArrayList<Rectangle2D>();
for (Rectangle2D freeSpace: freeSpaces)
{
final Collection<Rectangle2D> newFragments = new ArrayList<Rectangle2D>();
if (freeSpace.intersectsLine(minX, minY, maxX, minY))
newFragments.add(new Rectangle2D.Double(freeSpace.getMinX(), freeSpace.getMinY(), freeSpace.getWidth(), minY-freeSpace.getMinY()));
if (freeSpace.intersectsLine(minX, maxY, maxX, maxY))
newFragments.add(new Rectangle2D.Double(freeSpace.getMinX(), maxY, freeSpace.getWidth(), freeSpace.getMaxY() - maxY));
if (freeSpace.intersectsLine(minX, minY, minX, maxY))
newFragments.add(new Rectangle2D.Double(freeSpace.getMinX(), freeSpace.getMinY(), minX - freeSpace.getMinX(), freeSpace.getHeight()));
if (freeSpace.intersectsLine(maxX, minY, maxX, maxY))
newFragments.add(new Rectangle2D.Double(maxX, freeSpace.getMinY(), freeSpace.getMaxX() - maxX, freeSpace.getHeight()));
if (newFragments.isEmpty())
{
add(newFreeSpaces, freeSpace);
}
else
{
for (Rectangle2D fragment: newFragments)
{
if (fragment.getHeight() >= minHeight && fragment.getWidth() >= minWidth)
{
add(newFreeSpaces, fragment);
}
}
}
}
freeSpaces = newFreeSpaces;
}
void add(Collection<Rectangle2D> rectangles, Rectangle2D addition)
{
final Collection<Rectangle2D> toRemove = new ArrayList<Rectangle2D>();
boolean isContained = false;
for (Rectangle2D rectangle: rectangles)
{
if (rectangle.contains(addition))
{
isContained = true;
break;
}
if (addition.contains(rectangle))
toRemove.add(rectangle);
}
rectangles.removeAll(toRemove);
if (!isContained)
rectangles.add(addition);
}
//
// hidden members
//
Path2D currentPath = new Path2D.Float();
Collection<Rectangle2D> freeSpaces = null;
final float minWidth;
final float minHeight;
}
Using this FreeSpaceFinder
you can find empty areas with given minimum dimensions in a method like this:
public Collection<Rectangle2D> find(PDDocument pdDocument, PDPage pdPage, float minWidth, float minHeight) throws IOException {
FreeSpaceFinder finder = new FreeSpaceFinder(pdPage, minWidth, minHeight);
finder.processPage(pdPage);
return finder.getFreeSpaces();
}
(DetermineFreeSpaces method find
)
Applied to the same PDF page as was the iText centric solution with minimum width 200 and height 50, we get:
Comparing to the analogous screen shot for the iText variant, we see that we get more possible rectangles here.
This is due to the iText solution using the font-level ascender and descender while we here use the individual glyph bounding boxes.