Swing : best way to implement zoom functionality in image component - java

I've create a SWING component JImageEditor which simply displays a picture. The idea is to add more functionality to the component further down the road.
One functionality which I've already implemented is zooming. Right now, this part is taken care of in the paintComponent() method. However, somehow I suspect that this might be a bad idea as this means the image will be scaled from the original size to the current "zoom size" each and every time paintComponent() is invoked. The paintComponent code goes as follows :
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
int w = getWidth();
int h = getHeight();
double imageWidth = scale * image.getWidth();
double imageHeight = scale * image.getHeight();
double x = (w - imageWidth) / 2;
double y = (h - imageHeight) / 2;
AffineTransform at = AffineTransform.getTranslateInstance(x, y);
at.scale(scale, scale);
g2.drawRenderedImage(image, at);
}
Now, what I though of as an alternative was to keep two instances of BufferedImage where one is the original and one is the current "view". That way I could handle the actual zooming/scaling whenever the setScale() method is invoked, instead of scaling in paintComponent(). However, the drawback is that i need to keep two instances of BufferedImage which will lead to higher memory consumption depending on the image size. It is, of course, impossible to predict how large images any given user will open using the component.
What I'm looking for is either a thumbs up if I am on the right track with my work, or thumbs down If it's bad design and some other solution should be considered. I appreciate all input, and will reward all answers which enlightens me :-)

I'd say put a timing section in your paintComponent to measure how long it takes. Get your base measure from what you have now. Then implement the optimized method with the extra BufferedImage. Compare the measurements and pick the one that is smaller. I have a feeling your intuition is correct that doing the affine transform every paint cycle is slow, and by creating a double buffer for the scaled image and the source will be faster. Although I can't find any thing out there that confirms or denies this, and it could be affected by hardware acceleration.
If you extracted that section of code into a ZoomableBufferedImage class you could easily turn on or off the optimized/unoptimized versions. The ZoomableBufferedImage would hold a reference to the source image, and contain a extra buffered image that it can keep the scaled version in. As you zoom in/out the ZoomableBufferedImage draws to the buffer or not based on its settings, then in it's paint method it can draw either from the buffer or by applying the AffineTransform to the source and drawing that based on it's settings.

Related

Swing issue: drawing rectangles over the pdf image and keep their size and positions consistent when zooming in and out

I have a rather pressing question in regards to Swing, which I haven't touched for many years.
I have a code that allows the user to draw different rectangles on the pdf document (contained within JPanel). I draw them, move them, resize them, select them, and even write text on them. What I can't do is to keep them consistent when I zoom the document. As the document gets bigger, the rectangles I've drawn stay at the same position and the same size.
I was wondering if there's a relatively easy logic to track the zooming level and, most importantly, update the rectangles accordingly. I can retrieve zoom factor, it's a float, but, unfortunately, I'm using Rectangle object, which uses int for x, y, height, and width. It will be a hassle to convert it to Rectangle.Float, and I wanted to save it for a last resort.
I've tried to use AffineTransform, but I'm not quite familiar with it, for some reason I'm getting the wrong coordinates for y. Can anyone explain to me:
What's the best way to control the Rectangle object, as the pdf document gets zoomed in and out?
If AffineTransform is the best way, how should I handle it (maybe there's a link to a good explanation, if so - I couldn't find it)?
This is the only issue I've been struggling with and it's getting a bit frustrating now.
To scale using an AffineTransform:
Get the transform T of the Graphics object G
Create an AffineTransform object A
Set the scale of A
Set the transform of the G to A
Draw the shapes
Set the transform of G back to T
Translated into code - assuming scale is the value to scale by:
#Override
protected void paintComponent(Graphics gr){
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
AffineTransform prevTransform = g.getTransform();
AffineTransform at = new AffineTransform(prevTransform);
at.scale(scale, scale);
g.setTransform(at);
g.drawRect(...);
g.setTransform(prevTransform);
}

image rotation method leaves something in the ram

I am currently working on a game and for more diversity of graphics I want to call a rotate(BufferedImage imgOld, int deg) method a few hundred times to rotate some graphics (eg. tree stumps).
With unrotated graphics I never had any issues with memory. But as soon as I started to use the rotator the error occurred if I didn't reduce the amount of rotated images drastically (like 95% drastically).
The error always occurred inside of the rotate(BufferedImage imgOld, int deg) method and only since I started to rotate massive amounts of images so I suppose there must be some leftovers inside the memory from the rotate(BufferedImage imgOld, int deg) method I don't know how to deal with.
Here you see the code of the class Rotator:
package movement;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
public class Rotator {
public static BufferedImage rotate(BufferedImage imgOld, int deg){ //Parameters for the method are the image to rotate and the rate of rotation in degrees
AffineTransform at = AffineTransform.getRotateInstance(Math.toRadians(deg), (int)(imgOld.getWidth()/2), (int)(imgOld.getHeight()/2)); //setting up the transform
BufferedImage imgNew = new BufferedImage(imgOld.getWidth(), imgOld.getHeight(), imgOld.getType()); //creating a new image with the same properties of the old one
Graphics2D g = (Graphics2D) imgNew.getGraphics(); //create my graphics
g.setTransform(at); //applying the transform
g.drawImage(imgOld, 0, 0, null); //painting rotated image
return imgNew; //return rotated image
}
}
I hope somebody has an idea and i didn't make any mistakes (its my first question posted here).
Your biggest problem is simple- you're duplicating every image you're every time you rotate. You can try to draw the rotated image back onto itself, removing the need to churn through a massive amount of objects for the garbage collector.
Also, if you are dead-set on making new objects, make sure you flush (BufferedImage#flush) the old images and dipose (Graphics2D#dispose) the graphics objects.
You are using the wrong tool for the job. If you want to transform entire images, esp. BufferedImages, you should have a look at BufferedImageOps and in your case AffineTransformOp.
Then the entire operation may be implemented as
public static BufferedImage rotate(BufferedImage imgOld, int deg){
AffineTransform at = AffineTransform.getRotateInstance(
Math.toRadians(deg), imgOld.getWidth()/2, imgOld.getHeight()/2);
AffineTransformOp op=new AffineTransformOp(at, AffineTransformOp.TYPE_BICUBIC);
BufferedImage imgNew = new BufferedImage(
imgOld.getWidth(), imgOld.getHeight(), imgOld.getType());
return op.filter(imgOld, imgNew);
}
This doesn’t deal with Graphics and such alike. The way your original approach works, is to rotate the original image and draw it onto the destination image by combining the destination’s original content (initially empty) with the rotated data of the source, depending on the Graphics blending mode. In contrast, the code above will directly render the transformed picture into the destination, not attempting to preserve existing content.
Note further, that you can pass in an existing image for reuse. BufferedImageOp.filter supports a null destination image argument in order to create a new compatible image but the AffineTransformOp would create an image bigger than the source for angles like 45° which seems not what you want, given the code in your question.

Performance of rescaling and filtering images repeatedly with drawImage(...) and solutions

I am making a grid-based game that resizes its grid as the window size changes. I also may apply color filters for lighting effects in the future. I am concerned about the performance of this code, which draws to the screen an image in one of the grid squares.
public void drawSquares(Graphics g){
ListIterator<Viewport> iterator = vp.listIterator();
while(iterator.hasNext()){
Viewport v = (Viewport)iterator.next();
BufferedImage img = v.getSqView().getImage();
Rectangle b = v.getPixRect();
g.drawImage(img, b.x, b.y, b.width, b.height, v.getSqView().getBackground(), null);
}
return;
}
What this code does is get the image (stored in img) and get the pixel rectangle it needs to fit in (stored in b), then draw it in the space alloted via drawImage.
drawImage says that it scales images on the fly - which means that all images are being rescaled every frame. But the window is only resized rarely, so this must waste lots of processor time doing the same thing over and over again.
Now I saw this and decided that I would just update all the images upon resizing once, then store the result and be able to draw normally.
Like this:
public void resizeImage(int width, int height){
BufferedImage resized = new BufferedImage(width, height, img.getType());
Graphics2D g = resized.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(img, 0, 0, width, height, 0, 0, img.getWidth(), img.getHeight(), null);
g.dispose();
img = resized;
}
This doesn't work - I think it has something to do with img = resized. I just lose all the images with this code.
I have a few questions then.
What is the performance cost of repeatedly scaling with drawImage? Is it any different even if the window has not been resized in between frames?
How should I get the second code snippet to work? What is going wrong?
If I apply a lighting filter to a tile, will that eat up tons of processor time as well if I run it each frame? (Think 225 or so small images on a 800x800 or so display)
What is best practice for applying lighting filters? I am planning on overlaying on the whole map a pitch black filter, then exposing the areas around light sources.
Thanks for any help with this!
Resize the frame of this Grid to get a subjective feel for the latency. Use the approach shown here to measure the latency. Verify your findings in a profiler.
There's no reason you shouldn't be able to resize the elements of a List<Image> as you propose, but add() the resized instances to a new list as they are created.
What is the performance cost of repeatedly scaling with drawImage? Is
it any different even if the window has not been resized in between
frames?
You should always measure, but there is definitely a performance cost here, even if the window is not resized, because as the Javadoc says, there is no caching behind this drawImage method. The cost also depends on the frame rate.
How should I get the second code snippet to work? What is going wrong?
The second code snippet should be OK, I think the problem is somewhere else. Try reproducing the problem in a "small but complete" program, and post another question if you still see the problem.
If I apply a lighting filter to a tile, will that eat up tons of processor time as well if I run it each frame? (Think 225 or so small images on a 800x800 or so display)
You should always measure :)
What is best practice for applying lighting filters? I am planning on overlaying on the whole map a pitch black filter, then exposing the areas around light sources.
You can use an AlphaComposite for this.

Rotating Image (getting it back to original position)

Using "Affine Transformation" I can rotate imageA easily. As well, imageA will move along with imageB. However, I cannot seem to find a way to move imageA back to its original position after I have rotated it.
(I have done some research on some sites and apparently the best method is to move the image back to its original position so that it looks like its rotating from an anchor point.)
Heres my code so far:
public void paintComponent(Graphics g) {
super.paintComponent(g);
AffineTransform af = new AffineTransform();
Graphics2D g2d = (Graphics2D) g;
af.translate(imageBx, imageBy); // moves ImageA to imageb's position
af.rotate(Math.toRadians(angle), imageB.getHeight(this) / 2, imageB.getWidth(this) / 2);
g2d.drawImage(imageA, af, null);
g2d.drawImage(imageB, imageBx, imageBy, null);
}
If anyone can help me move imageA back to its original position (which is right on imageB) that would extremely helpful!
I looked that over, but the code rotates the entire panel; I just want to rotate one Image on a fixed rotate point.
Two things may help guide your understanding:
The example cited uses rotate(double theta); it is preceeded by a translation to the origin and succeeded by a translation the panel's center. Note that the operations are performed in the apparent reverse of the declared order. Your example (may have meant to) invoke rotate(double theta, double anchorx, double anchory). The two effect the same result, the latter being a convenient alternative to the former.
This example contrasts how one can transform the graphics context (g2d) or the image itself. Your example invokes drawImage(Image img, AffineTransform xform, ImageObserver obs), which concatenates xform to the existing graphics transform; this affects all subsequent drawing. It may be easier to keep them separate.

Can we tilt a JPanel at an angle?

I have image inside the JPanel. I would like to rotate the image. Is it possible to rotate the JPanel using Graphics, Image is rotatable, Just out of curiosity is it possible to rotate JPanel ?
Yes! This is possible and fairly straightforward too. I haven't done rotations but I have done other affine transformations (scaling the entire GUI up and down) very successfully on a project. I cannot see why rotations should be any different.
Instead of trying to scale each component use the fact that you can set a transformation on the Graphics object. Since this is shared between all components being rendered you get all things transformed at once "for free". It is important to realize that the transformation is only a rendering-process-step ... i.e. all components still believe they have the bounds (locations+sizes) which you gave them in the untransformed world. This leaves us with the challenge to deal with mouse-events correctly. To do this you simply add a glass-pane in front of your main-panel. This pane collects all mouse-events and apply a reverse of the transform on the event and then sends the event onward towards all other components.
Conceptually very simple! Still, I remember it took some tweaking to get it all crisp though. Especially the fact that rendered texts (fonts) in java are not correctly linearly scaled (it scales in discrete steps corresponding to font-sizes) imposed a final challenge in my scale-affine-transformation-case. Maybe you don't have to worry about that if you only rotate.
I got my inspiration from JXTransformer: http://www.java.net/blog/alexfromsun/archive/2006/07/jxtransformer_t.html
As far as I know you can't rotate a JPanel itself but you might be able to rotate the image inside the JPanel using Java2D. Here's an article that might help.
Edit:
There might actually be a way to rotate JComponents (such as JPanel) if you override their paintXxx methods and use AffineTransform.
It's not possible to rotate JPanel itself, but it's certainly possible to rotate any image inside. There are quite a few ways to do that, you can - for example - override JPanel's public void paint(Graphics g) and then cast Graphics to Graphics2D. It's very useful class, does rotation and much more ;) Check api docs for more info about this one.
Yes, it is possible. But you won't rotate the panel, but the image:
public void paintComponent(Graphics gg)
{
Graphics2D g = (Graphics2D) gg;
g.setRenderingHint(RenderingHints.KEY_ANTI_ALIAS, RenderingHints.VALUE_ANTI_ALIAS_ON);
AfflineTransform matrix = g.getTransform(); // Backup
float angle = Math.PI / 4.0f; // 45°
g.rotate(angle);
/* Begin */
g.drawImage(yourImage, [your coordinates], null);
/* End */
g.setTranform(matrix); // Restore
}
Everything between /* Begin */ and /* End */ will be drawn rotated.
(I didn't test the code, so, they may be some syntax errors...)

Categories