I've been doing some experiments on pdfbox and I'm currently stuck on a issue which I suspect has something to do with coordinate system.
I'm extending PDFTextStripper to get the X and Y of each character in a pdf page.
Originally I was creating an Image with ImageIO printing the text at the position I received, and putting a little mark (rectangles with different colors) on the bottom of each reference I wanted, and everything seemed well.
But now to avoid losing the style from the pdf I just wanted to overlay the pdf and adding the previously spoken marks, but the coordinates I got don't match in PDPageContentStream.
Any help on matching pdf coordinates I get from PDFTextStripper -> processTextPosition to the visual coordinates
Using version 1.8.11
As discussed in the comments, this is the 1.8 version of the DrawPrintTextLocations tool that is part of the examples collections of the 2.0 version and which is based on the better known PrintTextLocations example. Unlike the 2.0 version, this one does not show the font bounding boxes, only the text extraction sizes, which is about the height of a small glyph (a, e, etc). It is used as an heuristic tool for text extraction. That is the cause for the "the textpositions i'm getting are halfed" effect here. If you need bounding boxes, better use 2.0 (which may be too big). To get exact sizes, you would have to calculate the path of each glyph and get the bounds of that one, again, you'd need the 2.0 version for that one.
public class DrawPrintTextLocations extends PDFTextStripper
{
private BufferedImage image;
private final String filename;
static final int SCALE = 4;
private Graphics2D g2d;
private final PDDocument document;
/**
* Instantiate a new PDFTextStripper object.
*
* #param document
* #param filename
* #throws IOException If there is an error loading the properties.
*/
public DrawPrintTextLocations(PDDocument document, String filename) throws IOException
{
this.document = document;
this.filename = filename;
}
/**
* This will print the documents data.
*
* #param args The command line arguments.
*
* #throws IOException If there is an error parsing the document.
*/
public static void main(String[] args) throws IOException
{
if (args.length != 1)
{
usage();
}
else
{
PDDocument document = null;
try
{
document = PDDocument.load(new File(args[0]));
DrawPrintTextLocations stripper = new DrawPrintTextLocations(document, args[0]);
stripper.setSortByPosition(true);
for (int page = 0; page < document.getNumberOfPages(); ++page)
{
stripper.stripPage(page);
}
}
finally
{
if (document != null)
{
document.close();
}
}
}
}
private void stripPage(int page) throws IOException
{
PDPage pdPage = (PDPage) document.getDocumentCatalog().getAllPages().get(page);
image = pdPage.convertToImage(BufferedImage.TYPE_INT_RGB, 72 * SCALE);
PDRectangle cropBox = pdPage.getCropBox();
g2d = image.createGraphics();
g2d.setStroke(new BasicStroke(0.1f));
g2d.scale(SCALE, SCALE);
setStartPage(page + 1);
setEndPage(page + 1);
Writer dummy = new OutputStreamWriter(new ByteArrayOutputStream());
writeText(document, dummy);
// beads in green
g2d.setStroke(new BasicStroke(0.4f));
List<PDThreadBead> pageArticles = pdPage.getThreadBeads();
for (PDThreadBead bead : pageArticles)
{
PDRectangle r = bead.getRectangle();
GeneralPath p = transform(r, Matrix.getTranslatingInstance(-cropBox.getLowerLeftX(), cropBox.getLowerLeftY()));
AffineTransform flip = new AffineTransform();
flip.translate(0, pdPage.findCropBox().getHeight());
flip.scale(1, -1);
Shape s = flip.createTransformedShape(p);
g2d.setColor(Color.green);
g2d.draw(s);
}
g2d.dispose();
String imageFilename = filename;
int pt = imageFilename.lastIndexOf('.');
imageFilename = imageFilename.substring(0, pt) + "-marked-" + (page + 1) + ".png";
ImageIO.write(image, "png", new File(imageFilename));
}
/**
* Override the default functionality of PDFTextStripper.
*/
#Override
protected void writeString(String string, List<TextPosition> textPositions) throws IOException
{
for (TextPosition text : textPositions)
{
System.out.println("String[" + text.getXDirAdj() + ","
+ text.getYDirAdj() + " fs=" + text.getFontSize() + " xscale="
+ text.getXScale() + " height=" + text.getHeightDir() + " space="
+ text.getWidthOfSpace() + " width="
+ text.getWidthDirAdj() + "]" + text.getCharacter());
// in red:
// show rectangles with the "height" (not a real height, but used for text extraction
// heuristics, it is 1/2 of the bounding box height and starts at y=0)
Rectangle2D.Float rect = new Rectangle2D.Float(
text.getXDirAdj(),
(text.getYDirAdj() - text.getHeightDir()),
text.getWidthDirAdj(),
text.getHeightDir());
g2d.setColor(Color.red);
g2d.draw(rect);
}
}
/**
* This will print the usage for this document.
*/
private static void usage()
{
System.err.println("Usage: java " + DrawPrintTextLocations.class.getName() + " <input-pdf>");
}
/**
* Transforms the given point by this matrix.
*
* #param x x-coordinate
* #param y y-coordinate
*/
private Point2D.Float transformPoint(Matrix m, float x, float y)
{
float[][] values = m.getValues();
float a = values[0][0];
float b = values[0][1];
float c = values[1][0];
float d = values[1][1];
float e = values[2][0];
float f = values[2][2];
return new Point2D.Float(x * a + y * c + e, x * b + y * d + f);
}
/**
* Returns a path which represents this rectangle having been transformed by the given matrix.
* Note that the resulting path need not be rectangular.
*/
private GeneralPath transform(PDRectangle r, Matrix matrix)
{
float x1 = r.getLowerLeftX();
float y1 = r.getLowerLeftY();
float x2 = r.getUpperRightX();
float y2 = r.getUpperRightY();
Point2D.Float p0 = transformPoint(matrix, x1, y1);
Point2D.Float p1 = transformPoint(matrix, x2, y1);
Point2D.Float p2 = transformPoint(matrix, x2, y2);
Point2D.Float p3 = transformPoint(matrix, x1, y2);
GeneralPath path = new GeneralPath();
path.moveTo((float) p0.getX(), (float) p0.getY());
path.lineTo((float) p1.getX(), (float) p1.getY());
path.lineTo((float) p2.getX(), (float) p2.getY());
path.lineTo((float) p3.getX(), (float) p3.getY());
path.closePath();
return path;
}
}
Related
My aim is to add a image to a pdf and write a text above this image. I have centered the image and the text should be center above the image with a little margin to the image.
Currently the image will be added and centered but the text is not centered.
Here my current code. The interesting part is where the method drawTitleAtTop will be called. Here i have added the height of the newly added image to the y postion plus a margin of 3. The x coordinate I calculate depending on the incoming text but there is some miscalculation. Any advice?
private static void addScaledImage(ImageData imgData, PDDocument pdDocument, Dimension thePdfDimension) {
ImageHelper helper = Scalr::resize;
byte[] scaledImage = ImageUtils.resizeImageKeepAspectRatio(helper, imgData.getImageBinary(), thePdfDimension.width);
PDRectangle rectangle = pdDocument.getPage(0).getMediaBox();
PDPage page = new PDPage(rectangle);
pdDocument.addPage(page);
PDImageXObject pdImage = null;
try {
pdImage = PDImageXObject.createFromByteArray(pdDocument, scaledImage, null);
LOG.debug("size of scaled image is x: {0} y {1}", pdImage.getWidth(), pdImage.getHeight());
int xForImage = (thePdfDimension.width - pdImage.getWidth()) / 2 ;
int yForImage = (thePdfDimension.height - pdImage.getHeight()) / 2;
LOG.debug("new x {0} new y {1}", xForImage, yForImage);
try (PDPageContentStream contentStream = new PDPageContentStream(pdDocument, page, AppendMode.APPEND, true, true)) {
if (StringUtils.isNotBlank(imgData.getTitle())) {
yForImage = xForImage - 20;
contentStream.drawImage(pdImage, xForImage, yForImage, pdImage.getWidth(), pdImage.getHeight());
drawTitelAtTop(imgData, page, xForImage , yForImage + pdImage.getHeight() + 3, contentStream);
} else {
contentStream.drawImage(pdImage, xForImage, yForImage, pdImage.getWidth(), pdImage.getHeight());
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void drawTitelAtTop(ImageData imgData, PDPage page, int x, int y, PDPageContentStream contentStream) throws IOException {
PDFont font = PDType1Font.COURIER;
int fontSize = FONT_SIZE_FOR_TITLE;
float titleWidth = font.getStringWidth(imgData.getTitle()) / 1000 * fontSize;
LOG.debug("title width is " + titleWidth);
contentStream.setFont(font, fontSize);
contentStream.beginText();
float tx = ((x - titleWidth) / 2) + x;
//float tx = x;
//float ty = page.getMediaBox().getHeight() - marginTop + (marginTop / 4);
float ty = y;
LOG.debug("title offset x {0} y {1}", tx, ty);
contentStream.newLineAtOffset(tx,
ty);
contentStream.showText(imgData.getTitle());
contentStream.endText();
}
I'm trying to create an application that lets users modify pictures and then save them. I'm having trouble with the saving part.
This is the method that rotates the picture:
public void process(ImageView imageView) {
if(imageView.getImage() != null){
BufferedImage img;
img = Home.img;
double rads = Math.toRadians(90);
double sin = Math.abs(Math.sin(rads)), cos = Math.abs(Math.cos(rads));
int w = img.getWidth();
int h = img.getHeight();
int newWidth = (int) Math.floor(w * cos + h * sin);
int newHeight = (int) Math.floor(h * cos + w * sin);
BufferedImage rotated = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = rotated.createGraphics();
AffineTransform at = new AffineTransform();
at.translate((newWidth - w) / 2, (newHeight - h) / 2);
int x = w / 2;
int y = h / 2;
at.rotate(rads, x, y);
g2d.setTransform(at);
g2d.drawImage(img, 0, 0,null);
g2d.dispose();
imageView.setImage(convertToFxImage(rotated));
Home.img = rotated;
}
}
It sets the image in the Home controller class's imageView and also sets a static field to the modified image. Then I try to save it inside the Home class:
savAs.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
File dir = fileChooser.showSaveDialog(opButton.getScene().getWindow());
if (dir != null) {
try {
ImageIO.write(img, dir.getAbsolutePath().substring(dir.getAbsolutePath().length() - 3), dir);
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
This for some reason doesn't work. No IOException is thrown, but it doesn't create any file. When I try to save without modifying the image it works. Any idea why?
The method in ImageIO you call is specified below and it returns a boolean status code that you ignore:
public static boolean write(RenderedImage im,
String formatName,
File output) throws IOException
You haven't included details of the File so there is no way of telling if you have passed in a valid formatName which you derive from the last three characters of the output filename. Note that formatName isn't always 3 characters and should be lowercase - so both "FILE.JPG" and "FILE.JPEG" may fail to save.
If you believe the file extension is correct and is supported by the BufferedImage.TYPE_INT_ARGB format you've used, try change to use lowercase format, then check the result:
String format = dir.getAbsolutePath().substring(dir.getAbsolutePath().length() - 3).toLowerCase();
boolean ok = ImageIO.write(img, format, dir);
if (!ok)
throw new RuntimeException("Failed to write "+format+" to " + dir.getAbsolutePath());
If the BufferedImage format isn't compatible with the file extension, try using new BufferedImage(... , BufferedImage.TYPE_INT_RGB) or a different format type (eg PNG).
i'm trying to extract text with coordinates from a pdf file using PDFBox.
I mixed some methods/info found on internet (stackoverflow too), but the problem i have the coordinates doesnt'seems to be right. When i try to use coordinates for drawing a rectangle on top of tex, for example, the rect is painted elsewhere.
This is my code (please don't judge the style, was written very fast just to test)
TextLine.java
import java.util.List;
import org.apache.pdfbox.text.TextPosition;
/**
*
* #author samue
*/
public class TextLine {
public List<TextPosition> textPositions = null;
public String text = "";
}
myStripper.java
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
/**
*
* #author samue
*/
public class myStripper extends PDFTextStripper {
public myStripper() throws IOException
{
}
#Override
protected void startPage(PDPage page) throws IOException
{
startOfLine = true;
super.startPage(page);
}
#Override
protected void writeLineSeparator() throws IOException
{
startOfLine = true;
super.writeLineSeparator();
}
#Override
public String getText(PDDocument doc) throws IOException
{
lines = new ArrayList<TextLine>();
return super.getText(doc);
}
#Override
protected void writeWordSeparator() throws IOException
{
TextLine tmpline = null;
tmpline = lines.get(lines.size() - 1);
tmpline.text += getWordSeparator();
super.writeWordSeparator();
}
#Override
protected void writeString(String text, List<TextPosition> textPositions) throws IOException
{
TextLine tmpline = null;
if (startOfLine) {
tmpline = new TextLine();
tmpline.text = text;
tmpline.textPositions = textPositions;
lines.add(tmpline);
} else {
tmpline = lines.get(lines.size() - 1);
tmpline.text += text;
tmpline.textPositions.addAll(textPositions);
}
if (startOfLine)
{
startOfLine = false;
}
super.writeString(text, textPositions);
}
boolean startOfLine = true;
public ArrayList<TextLine> lines = null;
}
click event on AWT button
private void jButton1MouseClicked(java.awt.event.MouseEvent evt) {
// TODO add your handling code here:
try {
File file = new File("C:\\Users\\samue\\Desktop\\mwb_I_201711.pdf");
PDDocument doc = PDDocument.load(file);
myStripper stripper = new myStripper();
stripper.setStartPage(1); // fix it to first page just to test it
stripper.setEndPage(1);
stripper.getText(doc);
TextLine line = stripper.lines.get(1); // the line i want to paint on
float minx = -1;
float maxx = -1;
for (TextPosition pos: line.textPositions)
{
if (pos == null)
continue;
if (minx == -1 || pos.getTextMatrix().getTranslateX() < minx) {
minx = pos.getTextMatrix().getTranslateX();
}
if (maxx == -1 || pos.getTextMatrix().getTranslateX() > maxx) {
maxx = pos.getTextMatrix().getTranslateX();
}
}
TextPosition firstPosition = line.textPositions.get(0);
TextPosition lastPosition = line.textPositions.get(line.textPositions.size() - 1);
float x = minx;
float y = firstPosition.getTextMatrix().getTranslateY();
float w = (maxx - minx) + lastPosition.getWidth();
float h = lastPosition.getHeightDir();
PDPageContentStream contentStream = new PDPageContentStream(doc, doc.getPage(0), PDPageContentStream.AppendMode.APPEND, false);
contentStream.setNonStrokingColor(Color.RED);
contentStream.addRect(x, y, w, h);
contentStream.fill();
contentStream.close();
File fileout = new File("C:\\Users\\samue\\Desktop\\pdfbox.pdf");
doc.save(fileout);
doc.close();
} catch (Exception ex) {
}
}
any suggestion? what am i doing wrong?
This is just another case of the excessive PdfTextStripper coordinate normalization. Just like you I had thought that by using TextPosition.getTextMatrix() (instead of getX() and getY) one would get the actual coordinates, but no, even these matrix values have to be corrected (at least in PDFBox 2.0.x, I haven't checked 1.8.x) because the matrix is multiplied by a translation making the lower left corner of the crop box the origin.
Thus, in your case (in which the lower left of the crop box is not the origin), you have to correct the values, e.g. by replacing
float x = minx;
float y = firstPosition.getTextMatrix().getTranslateY();
by
PDRectangle cropBox = doc.getPage(0).getCropBox();
float x = minx + cropBox.getLowerLeftX();
float y = firstPosition.getTextMatrix().getTranslateY() + cropBox.getLowerLeftY();
Instead of
you now get
Obviously, though, you will also have to correct the height somewhat. This is due to the way the PdfTextStripper determines the text height:
// 1/2 the bbox is used as the height todo: why?
float glyphHeight = bbox.getHeight() / 2;
(from showGlyph(...) in LegacyPDFStreamEngine, the parent class of PdfTextStripper)
While the font bounding box indeed usually is too large, half of it often is not enough.
The following code worked for me:
// Definition of font baseline, ascent, descent: https://en.wikipedia.org/wiki/Ascender_(typography)
//
// The origin of the text coordinate system is the top-left corner where Y increases downward.
// TextPosition.getX(), getY() return the baseline.
TextPosition firstLetter = textPositions.get(0);
TextPosition lastLetter = textPositions.get(textPositions.size() - 1);
// Looking at LegacyPDFStreamEngine.showGlyph(), ascender and descender heights are calculated like
// CapHeight: https://stackoverflow.com/a/42021225/14731
float ascent = firstLetter.getFont().getFontDescriptor().getAscent() / 1000 * lastLetter.getFontSize();
Point topLeft = new Point(firstLetter.getX(), firstLetter.getY() - ascent);
float descent = lastLetter.getFont().getFontDescriptor().getDescent() / 1000 * lastLetter.getFontSize();
// Descent is negative, so we need to negate it to move downward.
Point bottomRight = new Point(lastLetter.getX() + lastLetter.getWidth(),
lastLetter.getY() - descent);
float descender = lastLetter.getFont().getFontDescriptor().getDescent() / 1000 * lastLetter.getFontSize();
// Descender height is negative, so we need to negate it to move downward
Point bottomRight = new Point(lastLetter.getX() + lastLetter.getWidth(),
lastLetter.getY() - descender);
In other words, we are creating a bounding box from the font's ascender down to its descender.
If you want to render these coordinates with the origin in the bottom-left corner, see https://stackoverflow.com/a/28114320/14731 for more details. You'll need to apply a transform like this:
contents.transform(new Matrix(1, 0, 0, -1, 0, page.getHeight()));
I have a pdf file where-in I am adding a stamp to all it's pages.
But, the problem is, the stamp is added to the upper-left corner of each page. If, the page has text in that part, the stamp appears on the text.
My question is, 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?
I am using IText and Java 1.7.
The free space fider class and the distance calculation function are the same that is there in the accepted answer.
Following is the edited code I am using:
// The resulting PDF file
String RESULT = "K:\\DCIN_TER\\DCIN_EPU2\\CIRCUIT FROM BRANCH\\RAINBOW ORDERS\\" + jtfSONo.getText().trim() + "\\PADR Release\\Final PADR Release 1.pdf";
// Create a reader
PdfReader reader = new PdfReader("K:\\DCIN_TER\\DCIN_EPU2\\CIRCUIT FROM BRANCH\\RAINBOW ORDERS\\" + jtfSONo.getText().trim() + "\\PADR Release\\Final PADR Release.pdf");
// Create a stamper
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(RESULT));
// Loop over the pages and add a footer to each page
int n = reader.getNumberOfPages();
for(int i = 1; i <= n; i++)
{
Collection<Rectangle2D> rectangles = find(reader, 300, 100, n, stamper); // minimum width & height of a rectangle
Iterator itr = rectangles.iterator();
while(itr.hasNext())
{
System.out.println(itr.next());
}
if(!(rectangles.isEmpty()) && (rectangles.size() != 0))
{
Rectangle2D best = null;
double bestDist = Double.MAX_VALUE;
Point2D.Double point = new Point2D.Double(200, 400);
float x = 0, y = 0;
for(Rectangle2D rectangle: rectangles)
{
double distance = distance(rectangle, point);
if(distance < bestDist)
{
best = rectangle;
bestDist = distance;
x = (float) best.getX();
y = (float) best.getY();
int left = (int) best.getMinX();
int right = (int) best.getMaxX();
int top = (int) best.getMaxY();
int bottom = (int) best.getMinY();
System.out.println("x : " + x);
System.out.println("y : " + y);
System.out.println("left : " + left);
System.out.println("right : " + right);
System.out.println("top : " + top);
System.out.println("bottom : " + bottom);
}
}
getFooterTable(i, n).writeSelectedRows(0, -1, x, y, stamper.getOverContent(i)); // 0, -1 indicates 1st row, 1st column upto last row and last column
}
else
getFooterTable(i, n).writeSelectedRows(0, -1, 94, 140, stamper.getOverContent(i)); // bottom left corner
}
// Close the stamper
stamper.close();
// Close the reader
reader.close();
public Collection<Rectangle2D> find(PdfReader reader, float minWidth, float minHeight, int page, PdfStamper stamper) 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);
System.out.println("finder.freeSpaces : " + finder.freeSpaces);
return finder.freeSpaces;
}
// Create a table with page X of Y, #param x the page number, #param y the total number of pages, #return a table that can be used as footer
public static PdfPTable getFooterTable(int x, int y)
{
java.util.Date date = new java.util.Date();
SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy");
String month = sdf.format(date);
System.out.println("Month : " + month);
PdfPTable table = new PdfPTable(1);
table.setTotalWidth(120);
table.setLockedWidth(true);
table.getDefaultCell().setFixedHeight(20);
table.getDefaultCell().setBorder(Rectangle.TOP);
table.getDefaultCell().setBorder(Rectangle.LEFT);
table.getDefaultCell().setBorder(Rectangle.RIGHT);
table.getDefaultCell().setBorderColorTop(BaseColor.BLUE);
table.getDefaultCell().setBorderColorLeft(BaseColor.BLUE);
table.getDefaultCell().setBorderColorRight(BaseColor.BLUE);
table.getDefaultCell().setBorderWidthTop(1f);
table.getDefaultCell().setBorderWidthLeft(1f);
table.getDefaultCell().setBorderWidthRight(1f);
table.getDefaultCell().setHorizontalAlignment(Element.ALIGN_CENTER);
Font font1 = new Font(FontFamily.HELVETICA, 10, Font.BOLD, BaseColor.BLUE);
table.addCell(new Phrase("CONTROLLED COPY", font1));
table.getDefaultCell().setFixedHeight(20);
table.getDefaultCell().setBorder(Rectangle.LEFT);
table.getDefaultCell().setBorder(Rectangle.RIGHT);
table.getDefaultCell().setBorderColorLeft(BaseColor.BLUE);
table.getDefaultCell().setBorderColorRight(BaseColor.BLUE);
table.getDefaultCell().setBorderWidthLeft(1f);
table.getDefaultCell().setBorderWidthRight(1f);
table.getDefaultCell().setHorizontalAlignment(Element.ALIGN_CENTER);
Font font = new Font(FontFamily.HELVETICA, 10, Font.BOLD, BaseColor.RED);
table.addCell(new Phrase(month, font));
table.getDefaultCell().setFixedHeight(20);
table.getDefaultCell().setBorder(Rectangle.LEFT);
table.getDefaultCell().setBorder(Rectangle.RIGHT);
table.getDefaultCell().setBorder(Rectangle.BOTTOM);
table.getDefaultCell().setBorderColorLeft(BaseColor.BLUE);
table.getDefaultCell().setBorderColorRight(BaseColor.BLUE);
table.getDefaultCell().setBorderColorBottom(BaseColor.BLUE);
table.getDefaultCell().setBorderWidthLeft(1f);
table.getDefaultCell().setBorderWidthRight(1f);
table.getDefaultCell().setBorderWidthBottom(1f);
table.getDefaultCell().setHorizontalAlignment(Element.ALIGN_CENTER);
table.addCell(new Phrase("BLR DESIGN DEPT.", font1));
return table;
}
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:
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:
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:
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
As you see the paper plane and the table background colorations now also are taken into account.
My other answer focuses on the original question, i.e. how to find free space with given minimum dimensions on a page.
Since that answer had been written, the OP provided code trying to make use of that original answer.
This answer deals with that code.
The code has a number of shortcoming.
The choice of free space on a page depends on the number of pages in the document.
The reason for this is to be found at the start of the loop over the pages:
for(int i = 1; i <= n; i++)
{
Collection<Rectangle2D> rectangles = find(reader, 300, 100, n, stamper);
...
The OP surely meant i, not n there. The code as is always looks for free space on the last document page.
The rectangles are lower than they should be.
The reason for this is to be found in the retrieval and use of the rectangle coordinates:
x = (float) best.getX();
y = (float) best.getY();
...
getFooterTable(i, n).writeSelectedRows(0, -1, x, y, stamper.getOverContent(i));
The Rectangle2D methods getX and getY return the coordinates of the lower left rectangle corner; the PdfPTable methods writeSelectedRows, on the other hand, require the upper left rectangle corner. Thus, getMaxY should be used instead of getY.
I have a class that extends from Printable, it prints fine using the standard PrintJob, but i would like to move to DocPrintJob so i can listen to the status of the print (successful print etc).
My current code looks like this to create a print job and print
// define printer
AttributeSet attributes = new HashAttributeSet();
attributes.add(new Copies(1));
// get printerJob
PrinterJob printJob = PrinterJob.getPrinterJob();
PageFormat pageFormat = printJob.defaultPage();
// sizing (standard is 72dpi, so multiple inches by this)
Double height = 8d * 72d;
Double width = 3d * 72d;
Double margin = 0.1d * 72d;
// set page size
Paper paper = pageFormat.getPaper();
paper.setSize(width, height);
paper.setImageableArea(margin, margin, width - (margin * 2), height - (margin * 2));
// set orientation and paper
pageFormat.setOrientation(PageFormat.PORTRAIT);
pageFormat.setPaper(paper);
// create a book for printing
Book book = new Book();
book.append(new EventPrint(args.getEvent()), pageFormat);
// set book to what we are printing
printJob.setPageable(book);
// set print request attributes
PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet();
aset.add(OrientationRequested.PORTRAIT);
// now print
try {
printJob.print();
} catch (PrinterException e) {
e.printStackTrace();
}
Now to convert this to a DocPrintJob, i followed this link which told me how to convert from PrintJob to DocPrintJob. So now my code became this
PrintService[] services = PrinterJob.lookupPrintServices(); //list of printers
PrintService printService = services[0];
DocFlavor[] flavours = printService.getSupportedDocFlavors();
// may need to determine which printer to use
DocPrintJob printJob = printService.createPrintJob();
// get out printable
EventPrint eventPrint = new EventPrint(args.getEvent());
// convert printable to doc
Doc doc = new SimpleDoc(eventPrint, DocFlavor.SERVICE_FORMATTED.PRINTABLE, null);
// add eventlistener to printJob
printJob.addPrintJobListener(new PrintJobListener() {
#Override
public void printDataTransferCompleted(PrintJobEvent pje) {
Console.Log("Print data sent successfully");
}
#Override
public void printJobCompleted(PrintJobEvent pje) {
Console.Log("Print successful");
}
#Override
public void printJobFailed(PrintJobEvent pje) {
Console.Log("Print failed");
}
#Override
public void printJobCanceled(PrintJobEvent pje) {
Console.Log("Print cancelled");
}
#Override
public void printJobNoMoreEvents(PrintJobEvent pje) {
Console.Log("No more printJob events");
}
#Override
public void printJobRequiresAttention(PrintJobEvent pje) {
Console.Log("printJob requires attention");
}
});
PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet();
aset.add(new Copies(1));
try {
printJob.print(doc, aset);
} catch (Exception e) {
}
Now for some reason, my print just keeps executing (400+ times until i close application). I am not sure if it is because maybe i have not set the paper size, like i did when i was using PrintJob? Would that cause it? If so, how can i set the paperSize of the DocPrintJob as it doesnt have the methods the normal printJob has?
Anyone else faces this problem before?
EDIT: Here is my eventPrint class and the class it inherits from
PRINTABLEBASE.JAVA
public class PrintableBase {
public Graphics2D g2d;
public float x, y, imageHeight, imageWidth, maxY;
public enum Alignment { LEFT, RIGHT, CENTER };
public void printSetup(Graphics graphics, PageFormat pageFormat) {
// user (0,0) is typically outside the imageable area, so we must translate
// by the X and Y values in the pageFormat to avoid clipping
g2d = (Graphics2D) graphics;
g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
// get starter X and Y
x = 0;//(float)pageFormat.getImageableX();
// do not offset vertical as pushes ticket down too much
y = 0;//(float)pageFormat.getImageableY();
// get height and width of the printable image
imageWidth = (float)pageFormat.getImageableWidth();
imageHeight = (float)pageFormat.getImageableHeight();
// maximum that we can go vertically
maxY = y;
Console.Log("imageWidth:" + imageWidth);
Console.Log("imageHeight: " + imageHeight);
Console.Log("X: " + x);
Console.Log("Y: " + y);
}
public void setFont(Font font) {
g2d.setFont(font);
}
public float addSeparatorLine(float y, float imageWidth) {
String fontName = g2d.getFont().getName();
int fontStyle = g2d.getFont().getStyle();
int fontSize = g2d.getFont().getSize();
g2d.setFont(new Font("Arial", Font.PLAIN, 10));
y = drawFirstLine(g2d, "---------------------------------------------------------------------------------------", imageWidth, 0, y, Alignment.LEFT);
// revery font
g2d.setFont(new Font(fontName, fontStyle, fontSize));
return y + 5;
}
public BufferedImage scaleImage(BufferedImage sbi, int dWidth, int dHeight) {
BufferedImage dbi = null;
if(sbi != null) {
// calculate ratio between standard size and scaled
double wRatio = (double)dWidth / (double)sbi.getWidth();
double hRatio = (double)dHeight / (double)sbi.getHeight();
// use wRatio by default
double sWidth = (double)sbi.getWidth() * wRatio;
double sHeight = (double)sbi.getHeight() * wRatio;
// if hRation is small, use that, as image will be reduced by more
if (hRatio < wRatio) {
sWidth = (double)sbi.getWidth() * hRatio;
sHeight = (double)sbi.getHeight() * hRatio;
}
double sRatio = (wRatio < hRatio) ? wRatio : hRatio;
// now create graphic, of new size
dbi = new BufferedImage((int)sWidth, (int)sHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = dbi.createGraphics();
AffineTransform at = AffineTransform.getScaleInstance(sRatio, sRatio);
g.drawRenderedImage(sbi, at);
}
return dbi;
}
// This function will only add the first line of text and will not wrap
// useful for adding the ticket title.
// Returns the height of the text
public float drawFirstLine(Graphics2D g2, String text, float width, float x, float y, Alignment alignment) {
AttributedString attstring = new AttributedString(text);
attstring.addAttribute(TextAttribute.FONT, g2.getFont());
AttributedCharacterIterator paragraph = attstring.getIterator();
int paragraphStart = paragraph.getBeginIndex();
int paragraphEnd = paragraph.getEndIndex();
FontRenderContext frc = g2.getFontRenderContext();
LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
// Set break width to width of Component.
float breakWidth = width;
float drawPosY = y;
// Set position to the index of the first character in the paragraph.
lineMeasurer.setPosition(paragraphStart);
// Get lines until the entire paragraph has been displayed.
if (lineMeasurer.getPosition() < paragraphEnd) {
// Retrieve next layout. A cleverer program would also cache
// these layouts until the component is re-sized.
TextLayout layout = lineMeasurer.nextLayout(breakWidth);
// Compute pen x position.
float drawPosX;
switch (alignment){
case RIGHT:
drawPosX = (float) x + breakWidth - layout.getAdvance();
break;
case CENTER:
drawPosX = (float) x + (breakWidth - layout.getAdvance())/2;
break;
default:
drawPosX = (float) x;
}
// Move y-coordinate by the ascent of the layout.
drawPosY += layout.getAscent();
// Draw the TextLayout at (drawPosX, drawPosY).
layout.draw(g2, drawPosX, drawPosY);
// Move y-coordinate in preparation for next layout.
drawPosY += layout.getDescent() + layout.getLeading();
}
return drawPosY;
}
/**
* Draw paragraph.
*
* #param g2 Drawing graphic.
* #param text String to draw.
* #param width Paragraph's desired width.
* #param x Start paragraph's X-Position.
* #param y Start paragraph's Y-Position.
* #param dir Paragraph's alignment.
* #return Next line Y-position to write to.
*/
protected float drawParagraph (String text, float width, float x, float y, Alignment alignment){
AttributedString attstring = new AttributedString(text);
attstring.addAttribute(TextAttribute.FONT, g2d.getFont());
AttributedCharacterIterator paragraph = attstring.getIterator();
int paragraphStart = paragraph.getBeginIndex();
int paragraphEnd = paragraph.getEndIndex();
FontRenderContext frc = g2d.getFontRenderContext();
LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
// Set break width to width of Component.
float breakWidth = width;
float drawPosY = y;
// Set position to the index of the first character in the paragraph.
lineMeasurer.setPosition(paragraphStart);
// Get lines until the entire paragraph has been displayed.
while (lineMeasurer.getPosition() < paragraphEnd) {
// Retrieve next layout. A cleverer program would also cache
// these layouts until the component is re-sized.
TextLayout layout = lineMeasurer.nextLayout(breakWidth);
// Compute pen x position.
float drawPosX;
switch (alignment){
case RIGHT:
drawPosX = (float) x + breakWidth - layout.getAdvance();
break;
case CENTER:
drawPosX = (float) x + (breakWidth - layout.getAdvance())/2;
break;
default:
drawPosX = (float) x;
}
// Move y-coordinate by the ascent of the layout.
drawPosY += layout.getAscent();
// Draw the TextLayout at (drawPosX, drawPosY).
layout.draw(g2d, drawPosX, drawPosY);
// Move y-coordinate in preparation for next layout.
drawPosY += layout.getDescent() + layout.getLeading();
}
return drawPosY;
}
}
EVENTPRINT.JAVA
public class EventPrint extends PrintableBase implements Printable {
private Event event;
public EventPrint(Event event) {
this.event = event;
}
#Override
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex)
throws PrinterException {
// setup
super.printSetup(graphics, pageFormat);
// title
super.setFont(new Font("Tahoma", Font.BOLD, 16));
y = super.drawParagraph(event.getTitle(), imageWidth, x, y, Alignment.LEFT);
RETURN PAGE_EXISTS;
}
I would say that your problem is that you never tell the API that there are no more pages....
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex)
throws PrinterException {
// setup
super.printSetup(graphics, pageFormat);
// title
super.setFont(new Font("Tahoma", Font.BOLD, 16));
y = super.drawParagraph(event.getTitle(), imageWidth, x, y, Alignment.LEFT);
RETURN PAGE_EXISTS;
}
So, assuming you only want to print a single page, you could use something like...
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex)
throws PrinterException {
int result = NO_SUCH_PAGE;
if (pageIndex == 0) {
// setup
super.printSetup(graphics, pageFormat);
// title
super.setFont(new Font("Tahoma", Font.BOLD, 16));
y = super.drawParagraph(event.getTitle(), imageWidth, x, y, Alignment.LEFT);
result = PAGE_EXISTS;
}
RETURN result;
}
Otherwise the API doesn't know when to stop printing.
Bookable uses a different approach, in that each page of the book is only printed once and it will continue until all pages are printed. Printable is different, it will continue printing until you tell it to stop
Take a look at A Basic Printing Program for more details
I also encountered some issues when switching from PrinterJob to DocPrintJob. Your code seems fine to me, so maybe you are right with the page size. You can set the page size like this:
aset.add(new MediaPrintableArea(0, 0, width, height, MediaPrintableArea.MM));
But it depends on your printable object how to set this. I have created a bytearray (PDF) with iText and set the page size there. Then, I set the width and height in the above code to Integer.MAX_VALUE.
I can post my entire code when I get home. Hope it helps!