How can I can insert an image or stamp on a pdf where there is free space available like a density scanner

is there any method by which I can read each page and if there is no text in that part add the stamp else search for nearest available free space, just like what a density scanner does?

iText does not offer that functionality out of the box. Depending of what kind of content you want to evade, though, you might consider either rendering the page to an image and looking for white spots in the image or doing text extraction with a strategy that tries to find locations without text.

The first alternative, analyzing a rendered version of the page, would be the focus of a separate question as an image processing library would have to be chosen first.

There are a number of situations, though, in which that first alternative is not the best way to go. E.g. if you only want to evade text but not necessarily graphics (like watermarks), or if you also want to evade invisible text (which usually can be marked in a PDF viewer and, therefore, interfere with your addition).

The second alternative (using text and image extraction abilities of iText) can be the more appropriate approach in such situations.

Here a sample RenderListener for such a task:

public class FreeSpaceFinder implements RenderListener
{
    //
    // constructors
    //
    public FreeSpaceFinder(Rectangle2D initialBox, float minWidth, float minHeight)
    {
        this(Collections.singleton(initialBox), minWidth, minHeight);
    }

    public FreeSpaceFinder(Collection<Rectangle2D> initialBoxes, float minWidth, float minHeight)
    {
        this.minWidth = minWidth;
        this.minHeight = minHeight;

        freeSpaces = initialBoxes;
    }

    //
    // RenderListener implementation
    //
    @Override
    public void renderText(TextRenderInfo renderInfo)
    {
        Rectangle2D usedSpace = renderInfo.getAscentLine().getBoundingRectange();
        usedSpace.add(renderInfo.getDescentLine().getBoundingRectange());
        remove(usedSpace);
    }

    @Override
    public void renderImage(ImageRenderInfo renderInfo)
    {
        Matrix imageMatrix = renderInfo.getImageCTM();

        Vector image00 = rect00.cross(imageMatrix);
        Vector image01 = rect01.cross(imageMatrix);
        Vector image10 = rect10.cross(imageMatrix);
        Vector image11 = rect11.cross(imageMatrix);

        Rectangle2D usedSpace = new Rectangle2D.Float(image00.get(Vector.I1), image00.get(Vector.I2), 0, 0);
        usedSpace.add(image01.get(Vector.I1), image01.get(Vector.I2));
        usedSpace.add(image10.get(Vector.I1), image10.get(Vector.I2));
        usedSpace.add(image11.get(Vector.I1), image11.get(Vector.I2));

        remove(usedSpace);
    }

    @Override
    public void beginTextBlock() { }

    @Override
    public void endTextBlock() { }

    //
    // 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);
    }

    //
    // members
    //
    public Collection<Rectangle2D> freeSpaces = null;
    final float minWidth;
    final float minHeight;

    final static Vector rect00 = new Vector(0, 0, 1);
    final static Vector rect01 = new Vector(0, 1, 1);
    final static Vector rect10 = new Vector(1, 0, 1);
    final static Vector rect11 = new Vector(1, 1, 1);
}

Using this FreeSpaceFinder you can find empty areas with given minimum dimensions in a method like this:

public Collection<Rectangle2D> find(PdfReader reader, float minWidth, float minHeight, int page) throws IOException
{
    Rectangle cropBox = reader.getCropBox(page);
    Rectangle2D crop = new Rectangle2D.Float(cropBox.getLeft(), cropBox.getBottom(), cropBox.getWidth(), cropBox.getHeight());
    FreeSpaceFinder finder = new FreeSpaceFinder(crop, minWidth, minHeight);
    PdfReaderContentParser parser = new PdfReaderContentParser(reader);
    parser.processContent(page, finder);
    return finder.freeSpaces;
}

For your task you now have to choose from the returned rectangles the one which suits you best.

Beware, this code still may have to be tuned to your requirements:

  • It ignores clip paths, rendering modes, colors, and covering objects. Thus, it considers all text and all bitmap images, whether they are actually visible or not.
  • It does not consider vector graphics (because the iText parser package does not consider them).
  • It is not very optimized.

Applied to this PDF page:

Sample Invoice

with minimum width 200 and height 50, you get these rectangles:

x       y       w       h
000,000 000,000 595,000 056,423
000,000 074,423 595,000 168,681
000,000 267,304 314,508 088,751
000,000 503,933 351,932 068,665
164,296 583,598 430,704 082,800
220,803 583,598 374,197 096,474
220,803 583,598 234,197 107,825
000,000 700,423 455,000 102,396
000,000 700,423 267,632 141,577
361,348 782,372 233,652 059,628

or, more visually, here as rectangles on the page:

Sample Invoice with free rectangles at least 200x50 in size marked

The paper plane is a vector graphic and, therefore, ignored.

Of course you could also change the PDF rendering code to not draw stuff you want to ignore and to visibly draw originally invisible stuff which you want to ignore, and then use bitmap image analysis nonetheless…

EDIT

In his comments the OP asked how to find the rectangle in the rectangle collection returned by find which is nearest to a given point.

First of all there not necessarily is the nearest rectangle, there may be multiple.

That been said, one can choose a nearest rectangle as follows:

First one needs to calculate a distance between point and rectangle, e.g.:

double distance(Rectangle2D rectangle, Point2D point)
{
    double x = point.getX();
    double y = point.getY();
    double left = rectangle.getMinX();
    double right = rectangle.getMaxX();
    double top = rectangle.getMaxY();
    double bottom = rectangle.getMinY();

    if (x < left) // point left of rect
    {
        if (y < bottom) // and below
            return Point2D.distance(x, y, left, bottom);
        if (y > top) // and top
            return Point2D.distance(x, y, left, top);
        return left - x;
    }
    if (x > right) // point right of rect
    {
        if (y < bottom) // and below
            return Point2D.distance(x, y, right, bottom);
        if (y > top) // and top
            return Point2D.distance(x, y, right, top);
        return x - right;
    }
    if (y < bottom) // and below
        return bottom - y;
    if (y > top) // and top
        return y - top;
    return 0;
}

Using this distance measurement one can select a nearest rectangle using code like this for a Collection<Rectangle2D> rectangles and a Point2D point:

Rectangle2D best = null;
double bestDist = Double.MAX_VALUE;

for (Rectangle2D rectangle: rectangles)
{
    double distance = distance(rectangle, point);
    if (distance < bestDist)
    {
        best = rectangle;
        bestDist = distance;
    }
}

After this best contains a best rectangle.

For the sample document used above, this method returns the colored rectangles for the page corners and left and right centers:

Sample Invoice with free rectangles at least 200x50 in size marked and best rectangles filled

EDIT TWO

Since iText 5.5.6, the RenderListener interface has been extended as ExtRenderListener to also be signaled about Path construction and path drawing operations. Thus, the FreeSpaceFinder above could also be extended to handle paths:

//
// Additional ExtRenderListener methods
//
@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)
        remove(currentPathRectangle);
    currentPathRectangle = null;

    return null;
}

@Override
public void clipPath(int rule)
{
    // TODO Auto-generated method stub

}

Rectangle2D.Float currentPathRectangle = null;

(FreeSpaceFinderExt.java)

Using this class the result above is improved to

Sample Invoice with free rectangles at least 200x50 in size marked and best rectangles filled, extended render listener

As you see the paper plane and the table background colorations now also are taken into account.

Leave a Comment