I have a small problem and I'm trying for some time to find out a solution.
Long story short I have to remove the top part of each page from a pdf with itext. I managed to do this with CROPBOX, but the problem is that this will make the pages smaller by removing the top part.
Can someone help me to implement this so the page size remains the same. My idea would be to override the top page with a white rectangle, but after many tries I didn't manage to do this.
This is the current code I'm using to crop the page.
PdfRectangle rect = new PdfRectangle(55, 0, 1000, 1000);
PdfDictionary pageDict;
for (int curentPage = 2; curentPage <= pdfReader.getNumberOfPages(); curentPage++) {
pageDict = pdfReader.getPageN(curentPage);
pageDict.put(PdfName.CROPBOX, rect);
}
In your code sample, you are cropping the pages. This reduces the visible size of the page.
Based on your description, you don't want cropping. Instead you want clipping.
I've written an example that clips the content of all pages of a PDF by introducing a margin of 200 user units (that's quite a margin). The example is called ClipPdf and you can see a clipped page here: hero_clipped.pdf (the iText superhero has lost arms, feet and part of his head in the clipping process.)
public void manipulatePdf(String src, String dest) throws IOException, DocumentException {
PdfReader reader = new PdfReader(src);
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(dest));
int n = reader.getNumberOfPages();
PdfDictionary page;
PdfArray media;
for (int p = 1; p <= n; p++) {
page = reader.getPageN(p);
media = page.getAsArray(PdfName.CROPBOX);
if (media == null) {
media = page.getAsArray(PdfName.MEDIABOX);
}
float llx = media.getAsNumber(0).floatValue() + 200;
float lly = media.getAsNumber(1).floatValue() + 200;
float w = media.getAsNumber(2).floatValue() - media.getAsNumber(0).floatValue() - 400;
float h = media.getAsNumber(3).floatValue() - media.getAsNumber(1).floatValue() - 400;
String command = String.format(
"\nq %.2f %.2f %.2f %.2f re W n\nq\n",
llx, lly, w, h);
stamper.getUnderContent(p).setLiteral(command);
stamper.getOverContent(p).setLiteral("\nQ\nQ\n");
}
stamper.close();
reader.close();
}
Obviously, you need to study this code before using it. Once you understand this code, you'll know that this code will only work for pages that aren't rotated. If you understand the code well, you should have no problem adapting the example for rotated pages.
Update
The re operator constructs a rectangle. It takes four parameters (the values preceding the operator) that define a rectangle: the x coordinate of the lower-left corner, the y coordinate of the lower-left corner, the width and the height.
The W operator sets the clipping path. We have just drawn a rectangle; this rectangle will be used to clip the content that follows.
The n operator starts a new path. It discards the paths we've constructed so far. In this case, it prevents that the rectangle we have drawn (and that we use as clipping path) is actually drawn.
The q and Q operators save and restore the graphics state stack, but that's rather obvious.
All of this is explained in ISO-32000-1 (available online if you Google well) and in the book The ABC of PDF.
Related
I am using PdfExplicitDestination as a page number, for titles by reading the existing pdf content from the page,
but I need to point the focus on specific text content while click on the bookmark.
for (int page = 1; page <= pdf.getNumberOfPages(); page++) {
ITextExtractionStrategy strategy = new SimpleTextExtractionStrategy();
String currentText = PdfTextExtractor.getTextFromPage(pdf.getPage(page), strategy);
if (currentText.contains("title")) {
k.addDestination(PdfExplicitDestination.createXYZ(pdf.getPage(page), pdf.getPage(page).getPageSize().getLeft(), pdf.getPage(page).getPageSize().getTop(), 0));
//System.out.println(currentText);
}
}
I need to find the position of the title in the pdf page to set "float top" value.
PdfExplicitDestination.createXYZ(pageNum, left, top, zoom)
Can any one please help to get it from the existing content in the pdf.
This task can be approached in a number of ways. One of the way is to go over page content in "stripes" (rectangles with small height), and only consider content from such a small rectangle at a time. If you find a text piece in such rectangle then you know that somewhere between upper and lower bound of Y position given by the rectangle coordinates lies the desired text content. You can e.g. create the destination to point to the topmost coordinate in that case - it might be a bit above the desired text but the difference will be small depending on the rectangle height you select.
The following code snipped contains example implementation of the presented idea. There are two parameters - windowHeight which must be tall enough to fit a piece of content you are looking for, but the smaller this variable is the better accuracy you get in the result. Parameter step defines how many such rectangles of height windowHeight we will try on each page. The smaller the parameter is the better accuracy you get, but bigger parameter values optimize performance. Up to a specific use case to tweak those trade-offs.
final float windowHeight = 30;
final float step = 10;
for (int page = 1; page <= pdf.getNumberOfPages(); page++) {
Rectangle pageSize = pdf.getPage(page).getPageSize();
for (float upperPoint = pageSize.getHeight(); upperPoint > 0; upperPoint -= step) {
IEventFilter filter = new TextRegionEventFilter(new Rectangle(0, upperPoint - windowHeight, pageSize.getWidth(), windowHeight));
LocationTextExtractionStrategy strategy = new LocationTextExtractionStrategy();
FilteredTextEventListener listener = new FilteredTextEventListener(strategy, filter);
new PdfCanvasProcessor(listener).processPageContent(pdf.getPage(page));
if (strategy.getResultantText().contains("title")) {
float top = upperPoint; // This is the topmost point of the rectangle
break; // Break here not to capture same text twice
}
}
}
I am still a bit new to Java programming so sorry for the huge dump of text. I greatly appreciate you taking the time to read over my current problem!
I am working on software to help speed up the process of board game design using Java Swing. It takes a CSV file of cards for a game, lets you build a dummy card by placing where each column will render on the card and then automatically generating all of the cards in the CSV from these positions.
Many card games have symbols that represent something in the game and I want to be able to insert them in the middle of strings. I can currently replace an entire string with a symbol; as it checks if the string == a known rule, draw the symbol instead. However, I don't know how I would go about searching through a string for a specific set of characters. If it finds them, delete them from the string and then draw the corresponding symbol in it's place. A great example can be seen with the mana symbols on magic cards: https://cdn0.vox-cdn.com/uploads/chorus_asset/file/8039357/C0cIVZ5.png
So the string could be: Gain 1 {GOLD} at the start of each tun.
And it would need to replace {GOLD} with the picture of gold using the Rule class that contains the string to find and a buffered image to replace it with.
I would like this to work without using a hard limit on the size of the symbol, but that is not a hard requirement. The best solution would scale the symbol so it's height was the same of the text.
This method takes a buffered image (a card with no text) and overlays the text on top of the card.
//Will modify the buffered image with the placeables
static public BufferedImage buildCard(BufferedImage start, int whichCardID) {
//Copy so we don't lose our template
BufferedImage ni = deepCopy(start); //ni = new image
//The headers of the document
String[] headers = MainWindow.loadedCards.get(0);
//For each placeable, write down it's text
for(int i=0; i<headers.length; i++) {
//get current header
String currentHeader = headers[i];
//The Text
String theText = MainWindow.loadedCards.get(whichCardID)[i];
//The Settings
PlaceableSettings theSettings = MainWindow.placeableSettings.get(currentHeader);
//Make the change to the image
//ni = writeToImage(ni, theText, theSettings);
///////New below
boolean foundRule = false;
//see if we have a rule to draw a graphic instead
for(RuleMaster.Rule r : RuleMaster.rules) {
if(r.keyword.equals(theText)) {
//there is a rule for this!
ni = drawRuleToImage(ni, r, theSettings);
foundRule = true; //so we don't draw the current text
}
}
//No rules for this
//Make the change to the image if there are no rules
if(foundRule == false)
ni = writeToImage(ni, theText, theSettings);
}
return ni;
}
//Takes a buffered image and writes text into it at the location given
static public BufferedImage writeToImage(BufferedImage old, String text, PlaceableSettings setts) {
//make new blank graphics
BufferedImage bi = new BufferedImage(old.getWidth(), old.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = bi.createGraphics();
//write old image to it
g2d.drawImage(old, 0, 0, null); //null was set to "this" when this was not static | Note ion case this breaks
//write text on it
g2d.setPaint(setts.getColor());
g2d.setFont(setts.getFont());
//Setup word wrap
FontMetrics fm = g2d.getFontMetrics(setts.getFont());
// int rightSideBuffer = bi.getWidth() - 10;
//Rectangle2D rect = fm.getStringBounds(text, setts.getX(), rightSideBuffer, g2d); // try just -'ing the x slot from the width below
Rectangle2D rect = fm.getStringBounds(text, g2d); //this gets you the bounds for the entire image, need to remove space for x,y position
//TODO: Problem: this is always length 1
//Solution! No auto wrap, let the person define it as a setting
#SuppressWarnings("unchecked")
List<String> textList=StringUtils.wrap(text, fm, setts.getPixelsTillWrap() ); //width counted in # of characters
//g2d.drawString(text, setts.getX(), setts.getY()); //old draw with no wrap
for(int i=0; i< textList.size(); i++) {
g2d.drawString(textList.get(i), setts.getX(), setts.getY() + ( i*(setts.getFont().getSize() + 2/*Buffer*/)));
}
//!!DEBUG
if(EntryPoint.DEBUG) {
Random r = new Random();
g2d.setPaint(Color.RED);
g2d.drawString(Integer.toString(textList.size()), 100, 50+r.nextInt(250));
g2d.setPaint(Color.GREEN);
g2d.drawString(Double.toString(rect.getWidth()), 200, 50+r.nextInt(250));
g2d.setPaint(Color.PINK);
//g2d.drawString(Integer.toString(( ((int) rect.getWidth()) - setts.getX())), 100, 250+r.nextInt(100));
}
//cleanup
g2d.dispose();
return bi;
}
//Takes a buffered image and draws an image on it at the location given
static public BufferedImage drawRuleToImage(BufferedImage old, RuleMaster.Rule rule, PlaceableSettings theSettings) {
//make new blank graphics
BufferedImage bi = new BufferedImage(old.getWidth(), old.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = bi.createGraphics();
//write old image to it
g2d.drawImage(old, 0, 0, null); //null was set to "this" when this was not static | Note ion case this breaks
g2d.drawImage(rule.image, theSettings.getX(), theSettings.getY(), null);
//cleanup
g2d.dispose();
//System.exit(1);
return bi;
}
Each Rule just contains the string to replace and the image to replace it with.
static public class Rule{
//Text to look for
String keyword;
//image to replace it with
BufferedImage image;
public Rule (String key, BufferedImage img) {
keyword = key;
image = img;
}
}
I am attempting to design this as a tool for many people to use, so the text should be able to match whatever the user adds; though my current process has been to use strings such as "{M}" and that could be a standard.
Another big hurdle in this is that the text can wrap on the cards, which means that the strings and image need to wrap together in provided bounds.
Edit 1:
Had some thoughts and am going to try this approach. Still see a possible issue with the bounds when getting the 'next' half of the string drawn; but I believe this may work.
//If found a rule mid text:
//Split string in 2 at the rule match: strings 'start', and 'next'
//Calculate x and y for symbol
//x is the # of characters in ('start' % the word wrap width) +1 as the symbol is the next character, then multiply that by the character size of the font
//y is the integer of dividing the # of characters in 'start' by word wrap width multiplied by the font height
//Draw Start of String
//Draw symbol
//next x = sym.x + sym width //TODO next (x,y) math
I was able to solve the problem by warping ahead of time based on the text size. By using a known size of the image, it could be know ahead of time to get the wrap done right.
Then I looped through each line. I looked did a split in the string on the text that would be replaced. I them drew the first part of the string as normal. Calculated the width of the string using int pixelWidth = ni.getGraphics().getFontMetrics().stringWidth(splitString[j]);. Then drew the image at the same Y, but added the width of the previous string to the X. Then added the current width of the image to the X and continued looping; drawing images and strings as needed.
I am working with PDF shrinking and then watermarking it and for the same I am using itextpdf-5.5.1.jar. Here is the code which I use to shrink PDF. In code xPercentage and xPercentage value is 0.9f. When I shrink PDF having content table , content on the page is shrinking properly. When I go to table of content the bounding box of hyperlink is getting misplaced. I noticed that size of bounding box is same for Original and shrink output document. How do I shrink bounding box of hyperlink with respect to content?
public void shrinkPDF(String strFilePath , String strFileName) throws Exception{
PdfReader reader = new PdfReader(strFilePath+"//"+strFileName);
PdfStamper stamper = new PdfStamper(reader, new
FileOutputStream(strFilePath+"//Shrink_"+strFileName));
int n = reader.getNumberOfPages();
Map mpPDFLayer = stamper.getPdfLayers();
for (int p = 1; p <= n; p++) {
float offsetX = (reader.getPageSize(p).getWidth() * (1 - xPercentage)) / 2;
float offsetY = (reader.getPageSize(p).getHeight() * (1 - yPercentage)) / 2;
stamper.getUnderContent(p).setLiteral(
String.format("\nq %s 0 0 %s %s %s cm\nq\n",
xPercentage, yPercentage, offsetX, offsetY));
stamper.getOverContent(p).setLiteral("\nQ\nQ\n");
}
stamper.close();
reader.close();
}
Your code shrinks only the content but it does not accordingly move and shrink annotations. So what you have to do additionally is to iterate over the annotations of each page and shrink them.
This in particular means that you have to shrink and move the Rect annotation rectangle. Depending on the nature of the respective annotation, though, there also are other coordinate values in them, e.g. the QuadPoints in case of a link or the L endpoint coordinates of a line.
BTW, your content shrinking code makes assumptions on the origin of the user space coordinate system; it appears to assume that the origin is in the lower left of the crop box and that the crop box and the media box coincide.
I have an image (basically, I get raw image data as 1024x1024 pixels) and the position in lat/lon of the center pixel of the image.
Each pixel represents the same fixed pixel scale in meters (e.g. 30m per pixel).
Now, I would like to draw the image onto a map which uses the coordinate reference system "EPSG:4326" (WGS84).
When I draw it by defining just corners in lat/lon of the image, depending on a "image size in pixel * pixel scale" calculation and converting the distances from the center point to lat/lon coordinates of each corner, I suppose, the image is not correctly drawn onto the map.
By the term "not correctly drawn" I mean, that the image seems to be shifted and also the contents of the image are not at the map location, where I expected them to be.
I suppose this is the case because I "mix" a pixel scaled image and a "EPSG:4326" coordinate reference system.
Now, with the information I have given, can I transform the whole pixel matrix from fixed pixel scale base to a new pixel matrix in the "EPSG:4326" coordinate reference system, using Geotools?
Of course, the transformation must be dependant on the center position in lat/lon, that I have been given, and on the pixel scale.
I wonder if using something like this would point me into the correct direction:
MathTransform transform = CRS.findMathTransform(DefaultGeocentricCRS.CARTESIAN, DefaultGeographicCRS.WGS84, true);
DirectPosition2D srcDirectPosition2D = new DirectPosition2D(DefaultGeocentricCRS.CARTESIAN, degreeLat.getDegree(), degreeLon.getDegree());
DirectPosition2D destDirectPosition2D = new DirectPosition2D();
transform.transform(srcDirectPosition2D, destDirectPosition2D);
double transX = destDirectPosition2D.x;
double transY = destDirectPosition2D.y;
int kmPerPixel = mapImage.getWidth / 1024; // It is known to me that my map is 1024x1024km ...
double x = zeroPointX + ((transX * 0.001) * kmPerPixel);
double y = zeroPointY + (((transX * -1) * 0.001) * kmPerPixel);
(got this code from another SO thread and already modified it a little bit, but still wonder if this is the correct starting point for my problem.)
I only suppose that my original image coordinate reference system is of the type DefaultGeocentricCRS.CARTESIAN. Can someone confirm this?
And from here on, is this the correct start to use Geotools for this kind of problem solving, or am I on the complete wrong path?
Additionally, I would like to add that this would be used in a quiet dynamic system. So my image update would be about 10Hz and the transormations have to be performed accordingly often.
Again, is this initial thought of mine leading to a solution, or do you have other solutions for solving my problem?
Thank you very much,
Kiamur
This is not as simple as it might sound. You are essentially trying to define an area on a sphere (ellipsoid technically) using a flat square. As such there is no "correct" way to do it, so you will always end up with some distortion. Without knowing exactly where your image came from there is no way to answer this exactly but the following code provides you with 3 different possible answers:
The first two make use of GeoTools' GeodeticCalculator to calculate the corner points using bearings and distances. These are the blue "square" and the green "square" above. The blue is calculating the corners directly while the green calculates the edges and infers the corners from the intersections (that's why it is squarer).
final int width = 1024, height = 1024;
GeometryFactory gf = new GeometryFactory();
Point centre = gf.createPoint(new Coordinate(0,51));
WKTWriter writer = new WKTWriter();
//direct method
GeodeticCalculator calc = new GeodeticCalculator(DefaultGeographicCRS.WGS84);
calc.setStartingGeographicPoint(centre.getX(), centre.getY());
double height2 = height/2.0;
double width2 = width/2.0;
double dist = Math.sqrt(height2*height2+width2 *width2);
double bearing = 45.0;
Coordinate[] corners = new Coordinate[5];
for (int i=0;i<4;i++) {
calc.setDirection(bearing, dist*1000.0 );
Point2D corner = calc.getDestinationGeographicPoint();
corners[i] = new Coordinate(corner.getX(),corner.getY());
bearing+=90.0;
}
corners[4] = corners[0];
Polygon bbox = gf.createPolygon(corners);
System.out.println(writer.write(bbox));
double[] edges = new double[4];
bearing = 0;
for(int i=0;i<4;i++) {
calc.setDirection(bearing, height2*1000.0 );
Point2D corner = calc.getDestinationGeographicPoint();
if(i%2 ==0) {
edges[i] = corner.getY();
}else {
edges[i] = corner.getX();
}
bearing+=90.0;
}
corners[0] = new Coordinate( edges[1],edges[0]);
corners[1] = new Coordinate( edges[1],edges[2]);
corners[2] = new Coordinate( edges[3],edges[2]);
corners[3] = new Coordinate( edges[3],edges[0]);
corners[4] = corners[0];
bbox = gf.createPolygon(corners);
System.out.println(writer.write(bbox));
Another way to do this is to transform the centre point into a projection that is "flatter" and use simple addition to calculate the corners and then reverse the transformation. To do this we can use the AUTO projection defined by the OGC WMS Specification to generate an Orthographic projection centred on our point, this gives the red "square" which is very similar to the blue one.
String code = "AUTO:42003," + centre.getX() + "," + centre.getY();
// System.out.println(code);
CoordinateReferenceSystem auto = CRS.decode(code);
// System.out.println(auto);
MathTransform transform = CRS.findMathTransform(DefaultGeographicCRS.WGS84,
auto);
MathTransform rtransform = CRS.findMathTransform(auto,DefaultGeographicCRS.WGS84);
Point g = (Point)JTS.transform(centre, transform);
width2 *=1000.0;
height2 *= 1000.0;
corners[0] = new Coordinate(g.getX()-width2,g.getY()-height2);
corners[1] = new Coordinate(g.getX()+width2,g.getY()-height2);
corners[2] = new Coordinate(g.getX()+width2,g.getY()+height2);
corners[3] = new Coordinate(g.getX()-width2,g.getY()+height2);
corners[4] = corners[0];
bbox = gf.createPolygon(corners);
bbox = (Polygon)JTS.transform(bbox, rtransform);
System.out.println(writer.write(bbox));
Which solution to use is a matter of taste, and depends on where your image came from but I suspect that either the red or the blue will be best. If you need to do this at 10Hz then you will need to test them for speed, but I suspect that transforming the images will be the bottle neck.
Once you have your bounding box setup to your satisfaction you can convert you (unreferenced) image to a georeferenced coverage using:
GridCoverageFactory factory = CoverageFactoryFinder.getGridCoverageFactory(null);
GridCoverage2D gc = factory.create("name", image, new ReferencedEnvelope(bbox.getEnvelopeInternal(),DefaultGeographicCRS.WGS84));
String fileName = "myImage.tif";
AbstractGridFormat format = GridFormatFinder.findFormat(fileName);
File out = new File(fileName);
GridCoverageWriter writer = format.getWriter(out);
try {
writer.write(gc, null);
writer.dispose();
} catch (IllegalArgumentException | IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
I am working on a project where as part of statements I need to attach arbitrary PDF files. These PDF files need to be marked by a title and page numbering, in the top-right corner of the PDF file. This is a legal requirement as these attachments are referred to by their title and total number of pages from the statements.
I (naively) hacked together some code that appears to be working on PDF files with pages in the Portrait orientation (at least the PDF files I tested with). However when I use this code on pages in a Landscape orientation, the title and numbering isn't visible.
The code:
PdfContentByte canvas = pdfStamper.getOverContent( pageNr );
Phrase phrase = new Phrase( sb.toString( ), new Font( FontFamily.HELVETICA, 9f ) ); // sb holds title + page numbering
float width = ColumnText.getWidth( phrase );
ColumnText.showTextAligned ( // draw text top-right
canvas,
Element.ALIGN_LEFT,
phrase,
canvas.getPdfDocument( ).right( ) - width, //x
canvas.getPdfDocument( ).top( ) + 9, //y
0 //rotation
);
Examples:
Portrait where it appears to work:
Landscape where it doesn't work:
Questions:
Where did I go wrong?
Is it possible to write such a piece of code that does it right for all possible page orientations?
If so, how?
You are adding the content, but you are adding it at the wrong place. See PageSize of PDF always the same between landscape and portrait with itextpdf
Let's assume that you are working with an A4 page using portrait orientation. That pages measures 595 by 842 user units. 595 is the width; 842 is the height.
Now let's switch to landscape. This can be done in two different ways:
define a width of 595 and a height of 842, and a rotation of 90 degrees.
define a width of 842 and a height of 595.
Which way is used to define the landscape orientation will have an impact on the value of the right() and top() method. I am pretty sure that you are adding the header to the landscape pages, but you are adding them outside the visible area of the page.
For those interested, I ended up doing it as follows. This works for both Portrait and Landscape orientations. This uses the PdfReader.getPageSizeWithRotation method to get the proper page size.
private String pageText(int pageNr, int pageTotal) {
return ""; // generate string to display top-right of PDF here
}
private void addDocumentObjects(int pageNr, PdfReader pdfReader, PdfStamper pdfStamper) {
final float pageMargin = 25f;
final float textSize = 9f;
final float lineMargin = 5f;
Phrase phrase = new Phrase (
pageText(pageNr, pdfReader.getNumberOfPages()),
new Font(FontFamily.HELVETICA, textSize)
);
final float phraseWidth = ColumnText.getWidth(phrase);
PdfContentByte canvas = pdfStamper.getOverContent(pageNr);
com.itextpdf.text.Rectangle pageRectangle = pdfReader.getPageSizeWithRotation(pageNr);
// draw white background rectangle before adding text + line
canvas.setColorFill(BaseColor.WHITE);
canvas.rectangle (
pageRectangle.getRight(pageMargin) - phraseWidth, //x
pageRectangle.getTop(pageMargin), //y
phraseWidth, // width
textSize + lineMargin //height
);
canvas.fill();
// draw text top right
canvas.setColorFill(BaseColor.BLACK);
ColumnText.showTextAligned (
canvas, //canvas
Element.ALIGN_LEFT, //alignment
phrase, //phrase
pageRectangle.getRight(pageMargin) - phraseWidth, //x
pageRectangle.getTop(pageMargin), //y
0 //rotation
);
// draw line under text
canvas.setColorStroke(BaseColor.BLACK);
canvas.setLineWidth(1);
canvas.moveTo (
pageRectangle.getRight(pageMargin) - phraseWidth, //x
pageRectangle.getTop(pageMargin) - lineMargin //y
);
canvas.lineTo (
pageRectangle.getRight(pageMargin), //x
pageRectangle.getTop(pageMargin) - lineMargin //y
);
canvas.stroke();
}