Related
I have the following method that "resizes" all the pages of a document to A4 page dimensions:
for (PdfDocument doc : pdfDocuments) {
int n = doc.getNumberOfPages();
for (int i = 1; i <= n; i++) {
PdfPage page = doc.getPage(i);
Rectangle media = page.getCropBox();
if (media == null) {
media = page.getMediaBox();
}
Rectangle crop = new Rectangle(0, 0, 210, 297);
page.setMediaBox(crop);
page.setCropBox(crop);
// The content, placed on a content stream before, will be rendered before the other content
// and, therefore, could be understood as a background (bottom "layer")
new PdfCanvas(page.newContentStreamBefore(),
page.getResources(), doc).writeLiteral("\nq 0.5 0 0 0.5 0 0 cm\nq\n");
// The content, placed on a content stream after, will be rendered after the other content
// and, therefore, could be understood as a foreground (top "layer")
new PdfCanvas(page.newContentStreamAfter(),
page.getResources(), doc).writeLiteral("\nQ\nQ\n");
}
}
However , this is not working as expected , the pages are being transformed to A4 (297x210) BUT the content is not being fitted inside (scaled) , the content appears cutted because the original pages are larger than 297X210 . How can I fix this ?
In a comment you clarified
I want the bounding box of the former content be scaled and a margin be added in the target
So, we first have to determine the bounding box of the original page content. This can be done using the MarginFinder class from this answer. Beware: That class determines the bounding box of all content, even if it is merely a white rectangle visually not distinct from no content or something formerly outside the crop box... If your use case requires it, you may have to extend that class to take such circumstances into consideration, too.
With the content bounding box determined all that remains to do is a bit of calculation.
The following method determines the bounding box using the class above, transforms the content accordingly, and changes the result crop box.
void scale(PdfDocument pdfDocument, Rectangle pageSize, Rectangle pageBodySize) {
int n = pdfDocument.getNumberOfPages();
for (int i = 1; i <= n; i++) {
PdfPage page = pdfDocument.getPage(i);
MarginFinder marginFinder = new MarginFinder();
PdfCanvasProcessor pdfCanvasProcessor = new PdfCanvasProcessor(marginFinder);
pdfCanvasProcessor.processPageContent(page);
Rectangle boundingBox = marginFinder.getBoundingBox();
if (boundingBox == null || boundingBox.getWidth() == 0 || boundingBox.getHeight() == 0) {
System.err.printf("Cannot scale page %d contents with bounding box %s\n", i , boundingBox);
continue;
} else {
// Scale and move content into A4 with margin
double scale = 0, xDiff= 0, yDiff = 0;
double xScale = pageBodySize.getWidth()/boundingBox.getWidth();
double yScale = pageBodySize.getHeight()/boundingBox.getHeight();
if (xScale < yScale) {
yDiff = boundingBox.getHeight() * (yScale / xScale - 1) / 2;
scale = xScale;
} else {
xDiff = boundingBox.getWidth() * (xScale / yScale - 1) / 2;
scale = yScale;
}
AffineTransform transform = AffineTransform.getTranslateInstance(pageBodySize.getLeft() + xDiff, pageBodySize.getBottom() + yDiff);
transform.scale(scale, scale);
transform.translate(-boundingBox.getLeft(), -boundingBox.getBottom());
new PdfCanvas(page.newContentStreamBefore(), page.getResources(), pdfDocument)
.concatMatrix(transform);
}
page.setMediaBox(pageSize);
page.setCropBox(pageSize);
}
}
(ScaleToA4 method scale)
For an A4 result page size with an inch of margin on each side you can call it like this for a PdfDocument pdfDocument:
Rectangle pageSize = PageSize.A4;
Rectangle pageBodySize = pageSize.clone().applyMargins(72, 72, 72, 72, false);
scale(pdfDocument, pageSize, pageBodySize);
(excerpt from ScaleToA4 test testFdaRequiresUseOfEctdFormatAndStandardizedStudyDataInFutureRegulatorySubmissionsSept)
I'm trying to draw some text on bitmap with a fixed position (Bottom left corner) no matter how bitmap size different.
Code below works but, the Text is drawn on the center of the bitmap
public Bitmap drawTextToBitmap(Context gContext,
Bitmap bitmap,
String gText) {
Resources resources = gContext.getResources();
float scale = resources.getDisplayMetrics().density;
android.graphics.Bitmap.Config bitmapConfig =
bitmap.getConfig();
if (bitmapConfig == null) {
bitmapConfig = android.graphics.Bitmap.Config.ARGB_8888;
}
bitmap = bitmap.copy(bitmapConfig, true);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(getResources().getColor(R.color.fujiColor));
paint.setTypeface(Typeface.createFromAsset(getAssets(), "fonts/DS-DIGI.TTF"));
paint.setTextSize((int) (14 * scale));
paint.setShadowLayer(1f, 0f, 1f, getResources().getColor(R.color.fujiShadowColor));
Rect bounds = new Rect();
paint.getTextBounds(gText, 0, gText.length(), bounds);
int x = (bitmap.getWidth() - bounds.width()) / 2;
int y = (bitmap.getHeight() + bounds.height()) / 2;
canvas.drawText(gText, x, y, paint);
return bitmap;
}
What I need is something similar to this :
Thank you.
As mentioned in the official docs, the text is drawn taking the (x,y) values as origin. Change the x,y values. Something along the following lines should work.
int horizontalSpacing = 24;
int verticalSpacing = 36;
int x = horizontalSpacing;//(bitmap.getWidth() - bounds.width()) / 2;
int y = bitmap.getHeight()-verticalSpacing;//(bitmap.getHeight() + bounds.height()) / 2;
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'm trying to build a program which can remove an single-colored border form an image.
The border is always white but the width of the border on the left and right side might differ from the width of the border at the top and bottom. So the image I want to extract is centered within the source image.
So from the following image I want to extract the green rectangle.
At the moment I don't know how to start solving this problem.
UPDATE
So finally calsign's code snippet and some improvements on it, solves my problem. I realized that the border around the inner image may not be completely single colored but can vary slightly. This leads to the behavior that for some images were left with a small border.
I solved this problem by improving the comparison of the color of two pixels by comparing the color distance of the two colors with a threshold. When the distance is below the threshold then the colors are handled as equally.
public Bitmap cropBorderFromBitmap(Bitmap bmp) {
//Convenience variables
int width = bmp.getWidth();
int height = bmp.getHeight();
int[] pixels = new int[height * width];
//Load the pixel data into the pixels array
bmp.getPixels(pixels, 0, width, 0, 0, width, height);
int length = pixels.length;
int borderColor = pixels[0];
//Locate the start of the border
int borderStart = 0;
for(int i = 0; i < length; i ++) {
// 1. Compare the color of two pixels whether they differ
// 2. Check whether the difference is significant
if(pixels[i] != borderColor && !sameColor(borderColor, pixels[i])) {
Log.i(TAG,"Current Color: " + pixels[i]);
borderStart = i;
break;
}
}
//Locate the end of the border
int borderEnd = 0;
for(int i = length - 1; i >= 0; i --) {
if(pixels[i] != borderColor && !sameColor(borderColor, pixels[i])) {
Log.i(TAG,"Current Color: " + pixels[i]);
borderEnd = length - i;
break;
}
}
//Calculate the margins
int leftMargin = borderStart % width;
int rightMargin = borderEnd % width;
int topMargin = borderStart / width;
int bottomMargin = borderEnd / width;
//Create the new, cropped version of the Bitmap
bmp = Bitmap.createBitmap(bmp, leftMargin, topMargin, width - leftMargin - rightMargin, height - topMargin - bottomMargin);
return bmp;
}
private boolean sameColor(int color1, int color2){
// Split colors into RGB values
long r1 = (color1)&0xFF;
long g1 = (color1 >>8)&0xFF;
long b1 = (color1 >>16)&0xFF;
long r2 = (color2)&0xFF;
long g2 = (color2 >>8)&0xFF;
long b2 = (color2 >>16)&0xFF;
long dist = (r2 - r1) * (r2 - r1) + (g2 - g1) * (g2 - g1) + (b2 - b1) *(b2 - b1);
// Check vs. threshold
return dist < 200;
}
Perhaps not the best use of the APIs to find a solution, but the one that came to mind: directly modify the image's pixels.
You can get a Bitmap's pixels with getPixels() and then create a new, cropped Bitmap with createBitmap(). Then, it's just a matter of finding the dimensions of the border.
You can find the color of the border by accessing the pixel located at position 0, and then compare that value (an int) to the value of each proceeding pixel until your reach the border (the pixel that isn't that color). With a little bit of math, it can be done.
Here is some simple code that demonstrates the point:
private void cropBorderFromBitmap(Bitmap bmp) {
int[] pixels;
//Load the pixel data into the pixels array
bmp.getPixels(pixels, 0, width, 0, 0, width, height);
//Convenience variables
int width = bmp.getWidth();
int height = bmp.getHeight();
int length = pixels.length;
int borderColor = pixels[0];
//Locate the start of the border
int borderStart;
for(int i = 0; i < length; i ++) {
if(pixels[i] != borderColor) {
borderStart = i;
break;
}
}
//Locate the end of the border
int borderEnd;
for(int i = length - 1; i >= 0; i --) {
if(pixels[i] != borderColor) {
borderEnd = length - i;
break;
}
}
//Calculate the margins
int leftMargin = borderStart % width;
int rightMargin = borderEnd % width;
int topMargin = borderStart / width;
int bottomMargin = borderEnd / width;
//Create the new, cropped version of the Bitmap
bmp = createBitmap(bmp, leftMargin, topMargin, width - leftMargin - rightMargin, height - topMargin - bottomMargin);
}
This is untested and lacks error checking (e.g., what if the width is 0?), but it should serve as a proof-of-concept.
EDIT: I just realized that I failed to complete the getPixels() method. The wonders of testing your code... it's fixed now.
If the frame around your picture is uniform then all you need to do is investigate when the pixels in the image change.
But first thing's first - you need to have a BufferedImage object to work with. It's a class that allows you to traverse the bitmap of an image (http://docs.oracle.com/javase/6/docs/api/java/awt/image/BufferedImage.html).
If you have the image saved as a file you need to call this method:
BufferedImage bimage = ImageIO.read(new File(file));
Now you can fetch the bitmap array from the bimage:
bimage.getRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize)
like this:
int[] rgb = bimage.getRGB(0, 0, bimage.getWidth(), bimage.getHeight(), null, 0, bimage.getWidth());
There could be some issues here with ColorModel so be sure to read up on your documentation of how to fetch the appropriate rgb from different file types.
Now that you have the rgb array you should start searching how far the frame stretches out from the middle of the picture. Keep in mind that this a single dimensional array - all the lines are written here sequentially one after another - as if you sliced the picture into lines 1pixel heigh and glued them together to form one long line.
This actually works to our advantage because the first different pixel we encounter in this table will work as a great reference point.
So now we just do something like this:
int pixel1=0,pixel2=0, i=0;
while(pixel1==pixel2 && i<bimage.getWidth()*bimage.getHeight()){
pixel1=pixel2;
pixel2=rgb[i++];
}
So now if the frame of your image is uniform, the top offset is the same as the bottom offset and the left offset is the same as the right offset then the number in the variable i is very likely to be the first pixel in the green rectangle.
In order to know which row and which column it is you need the following code:
int row= i%bimage.getWidth();
int column= i - row*bimage.getWidth();
Now the problem is that you may have an image embedded in the frame that in it's left upper corner is of the same color as the frame - so for example an image of a green rectangle with white corners in a white frame. Is this the case?
You can use the public int getPixel (int x, int y) function which return for every pixel its color
It should be easy to run through the border lines and verify that the color is still the same
This is my solution:
private Bitmap cropBorderFromBitmap(Bitmap bmp) {
final int borderWidth = 10; //preserved border width
final int borderColor = -1; //WHITE
int width = bmp.getWidth();
int height = bmp.getHeight();
int[] pixels = new int[width * height];
bmp.getPixels(pixels, 0, width, 0, 0, width, height);
int minX = -1;
int minY = -1;
int maxX = -1;
int maxY = -1;
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
if(bmp.getPixel(x,y) != borderColor) {
minX = (minX == -1) ? x : Math.min(x, minX);
minY = (minY == -1) ? y : Math.min(y, minY);
maxX = (maxX == -1) ? x : Math.max(x, maxX);
maxY = (maxY == -1) ? y : Math.max(y, maxY);
}
}
}
minX = Math.max(0, minX - borderWidth);
maxX = Math.min(width, maxX + borderWidth);
minY = Math.max(0, minY - borderWidth);
maxY = Math.min(height, maxY + borderWidth);
//Create the new, cropped version of the Bitmap
return Bitmap.createBitmap(bmp, minX, minY, maxX - minX, maxY-minY);
}
I am currently attempting to draw images on the screen at a regular rate like in a video game.
Unfortunately, because of the rate at which the image is moving, some frames are identical because the image has not yet moved a full pixel.
Is there a way to provide float values to Graphics2D for on-screen position to draw the image, rather than int values?
Initially here is what I had done:
BufferedImage srcImage = sprite.getImage ( );
Position imagePosition = ... ; //Defined elsewhere
g.drawImage ( srcImage, (int) imagePosition.getX(), (int) imagePosition.getY() );
This of course thresholds, so the picture doesn't move between pixels, but skips from one to the next.
The next method was to set the paint color to a texture instead and draw at a specified position. Unfortunately, this produced incorrect results that showed tiling rather than correct antialiasing.
g.setRenderingHint ( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
BufferedImage srcImage = sprite.getImage ( );
g.setPaint ( new TexturePaint ( srcImage, new Rectangle2D.Float ( 0, 0, srcImage.getWidth ( ), srcImage.getHeight ( ) ) ) );
AffineTransform xform = new AffineTransform ( );
xform.setToIdentity ( );
xform.translate ( onScreenPos.getX ( ), onScreenPos.getY ( ) );
g.transform ( xform );
g.fillRect(0, 0, srcImage.getWidth(), srcImage.getHeight());
What should I do to achieve the desired effect of subpixel rendering of an Image in Java?
You can use a BufferedImage and AffineTransform, draw to the buffered image, then draw the buffered image to the component in the paint event.
/* overrides the paint method */
#Override
public void paint(Graphics g) {
/* clear scene buffer */
g2d.clearRect(0, 0, (int)width, (int)height);
/* draw ball image to the memory image with transformed x/y double values */
AffineTransform t = new AffineTransform();
t.translate(ball.x, ball.y); // x/y set here, ball.x/y = double, ie: 10.33
t.scale(1, 1); // scale = 1
g2d.drawImage(image, t, null);
// draw the scene (double percision image) to the ui component
g.drawImage(scene, 0, 0, this);
}
Check my full example here: http://pastebin.com/hSAkYWqM
You can composite the image yourself using sub-pixel accuracy, but it's more work on your part. Simple bilinear interpolation should work well enough for a game. Below is psuedo-C++ code for doing it.
Normally, to draw a sprite at location (a,b), you'd do something like this:
for (x = a; x < a + sprite.width; x++)
{
for (y = b; y < b + sprite.height; y++)
{
*dstPixel = alphaBlend (*dstPixel, *spritePixel);
dstPixel++;
spritePixel++;
}
dstPixel += destLineDiff; // Move to start of next destination line
spritePixel += spriteLineDiff; // Move to start of next sprite line
}
To do sub-pixel rendering, you do the same loop, but account for the sub-pixel offset like so:
float xOffset = a - floor (a);
float yOffset = b - floor (b);
for (x = floor(a), spriteX = 0; x < floor(a) + sprite.width + 1; x++, spriteX++)
{
for (y = floor(b), spriteY = 0; y < floor (b) + sprite.height + 1; y++, spriteY++)
{
spriteInterp = bilinearInterp (sprite, spriteX + xOffset, spriteY + yOffset);
*dstPixel = alphaBlend (*dstPixel, spriteInterp);
dstPixel++;
spritePixel++;
}
dstPixel += destLineDiff; // Move to start of next destination line
spritePixel += spriteLineDiff; // Move to start of next sprite line
}
The bilinearInterp() function would look something like this:
Pixel bilinearInterp (Sprite* sprite, float x, float y)
{
// Interpolate the upper row of pixels
Pixel* topPtr = sprite->dataPtr + ((floor (y) + 1) * sprite->rowBytes) + floor(x) * sizeof (Pixel);
Pixel* bottomPtr = sprite->dataPtr + (floor (y) * sprite->rowBytes) + floor (x) * sizeof (Pixel);
float xOffset = x - floor (x);
float yOffset = y - floor (y);
Pixel top = *topPtr + ((*(topPtr + 1) - *topPtr) * xOffset;
Pixel bottom = *bottomPtr + ((*(bottomPtr + 1) - *bottomPtr) * xOffset;
return bottom + (top - bottom) * yOffset;
}
This should use no additional memory, but will take additional time to render.
I successfully solved my problem after doing something like lawrencealan proposed.
Originally, I had the following code, where g is transformed to a 16:9 coordinate system before the method is called:
private void drawStar(Graphics2D g, Star s) {
double radius = s.getRadius();
double x = s.getX() - radius;
double y = s.getY() - radius;
double width = radius*2;
double height = radius*2;
try {
BufferedImage image = ImageIO.read(this.getClass().getResource("/images/star.png"));
g.drawImage(image, (int)x, (int)y, (int)width, (int)height, this);
} catch (IOException ex) {
Logger.getLogger(View.class.getName()).log(Level.SEVERE, null, ex);
}
}
However, as noted by the questioner Kaushik Shankar, turning the double positions into integers makes the image "jump" around, and turning the double dimensions into integers makes it scale "jumpy" (why the hell does g.drawImage not accept doubles?!). What I found working for me was the following:
private void drawStar(Graphics2D g, Star s) {
AffineTransform originalTransform = g.getTransform();
double radius = s.getRadius();
double x = s.getX() - radius;
double y = s.getY() - radius;
double width = radius*2;
double height = radius*2;
try {
BufferedImage image = ImageIO.read(this.getClass().getResource("/images/star.png"));
g.translate(x, y);
g.scale(width/image.getWidth(), height/image.getHeight());
g.drawImage(image, 0, 0, this);
} catch (IOException ex) {
Logger.getLogger(View.class.getName()).log(Level.SEVERE, null, ex);
}
g.setTransform(originalTransform);
}
Seems like a stupid way of doing it though.
Change the resolution of your image accordingly, there's no such thing as a bitmap with sub-pixel coordinates, so basically what you can do is create an in memory image larger than what you want rendered to the screen, but allows you "sub-pixel" accuracy.
When you draw to the larger image in memory, you copy and resample that into the smaller render visible to the end user.
For example: a 100x100 image and it's 50x50 resized / resampled counterpart:
See: http://en.wikipedia.org/wiki/Resampling_%28bitmap%29