Scale images as a single surface in Java 2D API - java

There is a method called scale(double sx, double sy) in Graphics2D in Java. But this method seems like to scale images as separate surfaces rather than a single surface. As a result, scaled images have sharp corners if original images have no extra width and height. The following screenshot demonstrates the problem:
Here is the code:
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class TestJava {
static int scale = 10;
public static class Test extends JPanel {
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.scale(scale, scale);
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
BufferedImage img = null;
try {
img = ImageIO.read(new File("Sprite.png"));
} catch (IOException e) {
e.printStackTrace();
}
g2.drawImage(img, null, 5, 5);
}
}
public static void main(String[] args) {
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
Test test = new Test();
test.setBackground(Color.WHITE);
frame.add(test);
frame.setSize(300, 350);
frame.setVisible(true);
}
}
One possible solution to the problem is original images having extra width and height (in this case "Sprite.png"). But this does not seem to be a good way to eliminate the problem. So I am seeking for a programmatic way in Java to solve this problem rather than using an image editor. What is the way to do so?

In your example it's not the image you scale, but you set a scaling transformation on the Graphics2D object which will be applied on all operations performed on that graphics context.
If you want to scale an image, you have 2 options. All I write below uses java.awt.Image, but since BufferedImage extends Image, all this applies to BufferedImage as well.
1. Image.getScaledInstance()
You can use the Image.getScaledInstance(int width, int height, int hints) method. The 3rd parameter (the hints) tells what scaling algorithm you want to use which will affect the "quality" of the scaled image. Possible values are:
SCALE_DEFAULT, SCALE_FAST, SCALE_SMOOTH, SCALE_REPLICATE, SCALE_AREA_AVERAGING
Try the SCALE_AREA_AVERAGING and the SCALE_SMOOTH for nicer scaled images.
// Scaled 3 times:
Image img2 = img.getScaledInstance(img.getWidth(null)*3, img.getHeight(null)*3,
Image.SCALE_AREA_AVERAGING);
// Tip: you should cache the scaled image and not scale it in the paint() method!
// To draw it at x=100, y=200
g2.drawImage(img2, 100, 200, null);
2. Graphics.drawImage()
You can use different Graphics.drawImage() overloads where you can specify the size of the scaled image. You can "control" the image quality with the KEY_INTERPOLATION rendering hint. It has 3 possible values:
VALUE_INTERPOLATION_NEAREST_NEIGHBOR, VALUE_INTERPOLATION_BILINEAR,
VALUE_INTERPOLATION_BICUBIC
The VALUE_INTERPOLATION_BILINEAR uses a bilinear interpolation algorithm of the 4 nearest pixels. The VALUE_INTERPOLATION_BICUBIC uses a cubic interpolation of the 9 nearby pixels.
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
// To draw image scaled 3 times, x=100, y=200:
g2.drawImage(img, 100, 200, img.getWidth(null)*3, img.getHeight(null)*3, null);
Removing sharp edges
If you want to avoid sharp edges around the image, you should write a loop to go over the pixels at the edge of the image, and set some kind of transparency, e.g. alpha=0.5 (or alpha=128). You might also do this on multiple rows/columns, e.g. 0.8 alpha for the edge, 0.5 alpha for the 2nd line and 0.3 alpha for the 3rd line.

An interesting question (+1). I think that it is not trivial to find a good solution for this: The interpolation when scaling up the image always happens inside the image, and I can not imagine a way to make it blur the scaled pixels outside the image.
This leads to fairly simple solution: One could add a 1-pixel-margin around the whole image. In fact, this is the programmatic way of the solution that you proposed yourself. The resuld would look like this:
(the left one is the original, and the right one has the additional 1-pixel-border)
Here as a MCVE, based on your example
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
public class ScaledPaint
{
static int scale = 10;
public static class Test extends JPanel
{
BufferedImage image = createTestImage();
BufferedImage imageWithMargin = addMargin(image);
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.scale(scale, scale);
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
g2.drawImage(image, 5, 5, null);
g2.drawImage(imageWithMargin, 30, 5, null);
}
}
private static BufferedImage createTestImage()
{
BufferedImage image =
new BufferedImage(20, 20, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.RED);
g.drawOval(0, 0, 19, 19);
g.dispose();
return image;
}
private static BufferedImage addMargin(BufferedImage image)
{
return addMargin(image, 1, 1, 1, 1);
}
private static BufferedImage addMargin(BufferedImage image,
int left, int right, int top, int bottom)
{
BufferedImage newImage =
new BufferedImage(
image.getWidth() + left + right,
image.getHeight() + top + bottom,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = newImage.createGraphics();
g.drawImage(image, left, top, null);
g.dispose();
return newImage;
}
private static BufferedImage convertToARGB(BufferedImage image)
{
BufferedImage newImage =
new BufferedImage(image.getWidth(), image.getHeight(),
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = newImage.createGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
return newImage;
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static void createAndShowGUI()
{
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
Test test = new Test();
test.setBackground(Color.WHITE);
frame.add(test);
frame.setSize(600, 350);
frame.setVisible(true);
}
}
But...
... one problem with this approach can already be seen in the screenshot: The image becomes larger. And you'll have to take this into account when painting the image. So if your original sprites all had a nice, predefined, easy-to-handle size like 16x32, they will afterwards have a size of 18x34, which is rather odd for a tile. This may not a problem, depending on how you are handling your tile sizes. But if it is a problem, one could think about possible solutions. One solution might be to ...
take the 16x32 input image
create a 16x32 output image
paint the 16x32 intput image into the region (1,1)-(15,31) of the output image
But considering the fact that in sprites of this size, every single pixel may be important, this may have undesirable effects as well...
An aside: Altough I assume that the code that you posted was only intended as a MCVE, I'd like to point out (for others who might read this question and the code) :
You shoud NOT load images in the paintComponent method
For efficient painting, any PNG that is loaded (particularly when it contains transparency) should be converted into an image with a known type. This can be done with the convertToARGB method in my code snippet.

Related

Java - Drawing a buffered image into printable vs drawing directly into printable produces fuzzy results

I am designing a Java program for printing some labels on an A4 sheet. I would like to create the labels in a separate class which returns a BufferedImage, then pass a collection of BufferedImages to another class that implements the Printable interface, which will then arrange them on the page and print them.
I can do all this, but am finding when drawing the BufferedImage into the printable, the finer elements, particularly text, are a bit fuzzy. If I draw directly into the graphics object provided by the Printable, then the results are much better, but this is inconvenient for the structure of the program.
I have tried setting the antialiasing hints for text and graphics, and also tried drawing double size image and then scaling it down when drawing into the Printable, but this produces worse results.
Below is a test program I wrote to illustrate the differences. On the output the text is clearly fuzzier when I use a BufferedImage of the same size as the square drawn into the Printable, and again when using a larger BufferedImage and scaled down.
package printerresolutiontest;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.Paper;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
public class PrinterResolutionTest implements Printable {
public static void main(String[] args) throws PrinterException {
PrinterResolutionTest printrestest = new PrinterResolutionTest();
PrinterJob pj = PrinterJob.getPrinterJob();
PageFormat pf = pj.defaultPage();
Paper paper = pf.getPaper();
paper.setImageableArea(0*72, 0*72, 8.3*72, 11.7*72);
pf.setPaper(paper);
pj.setPrintable(printrestest, pf);
Boolean PRINT_DIALOG_OK = pj.printDialog();
if (PRINT_DIALOG_OK) {
pj.print();
}
}
#Override
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
Graphics2D g2d = (Graphics2D) graphics;
BufferedImage img1 = new BufferedImage(144, 144, BufferedImage.TYPE_INT_RGB);
BufferedImage img2 = new BufferedImage(72, 72, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d_bigbuf = img1.createGraphics();
Graphics2D g2d_buf = img2.createGraphics();
if (pageIndex > 0) {
return NO_SUCH_PAGE;
}
/*************************
* Draw directly into the Printable graphics object
*/
g2d.setColor(Color.RED);
g2d.fillRect(200, 200, 72, 72);
g2d.setColor(Color.black);
g2d.drawString("test", 202, (72/2)+202);
/*************************
* Draw the same features
* into a BufferedImage
*/
g2d_buf.setColor(Color.RED);
g2d_buf.fillRect(0, 0, 72, 72);
g2d_buf.setColor(Color.black);
g2d_buf.drawString("test", 0, 72/2);
/*************************
* Draw a double sized square and
* increase font size accordingly
*/
g2d_bigbuf.setColor(Color.RED);
g2d_bigbuf.fillRect(0, 0, 144, 144);
g2d_bigbuf.setColor(Color.black);
Font currentfont = g2d_bigbuf.getFont();
Font newfont = currentfont.deriveFont(currentfont.getSize() * 2F);
g2d_bigbuf.setFont(newfont);
g2d_bigbuf.drawString("test", 0, 144/2);
// Draw the regular sided BufferedImage
g2d.drawImage(img2, 300, 200, null);
// Draw the larger BufferedImage and scale down by half
g2d.drawImage(img1.getScaledInstance(72, 72, Image.SCALE_AREA_AVERAGING), 400, 200, null);
return PAGE_EXISTS;
}
}
BufferedImage test PNG
Here is a test screen capture from a file I printed to PDF as an example of the difference. Ideally I would like to get the same image quality using the BufferedImage as I do when drawing directly into the Printable.
Screens and printers are very different media. On a screen you typically have a low resolution of about 72 to 92 dpi (pixels per inch). Each pixels can have a huge number of colors (24m or more). On a printer you usually have a high resolution (up to 2540 dpi), but only between 2 (b/w) and 6 colors. In order to create intermediate colors, the printer will you a rasterization technique, e.g. halftone.
The way you're creating the BufferedImage, you're heavily biased towards screen output: low resolution, use of intermediate colors for anti-aliasing etc. The result will be poor. Furthermore, the intention of your output is lost. The printer driver only sees a pixelmap. It doesn't know if and where text is and cannot optimize the output for it.
In order to improve the quality, you have mainly two options:
If you only print lines and text and only use the colors that printer can create without mixing colors, you can create a BufferedImage that exactly matches the printer resolution and colors. That should give you a decent quality.
Otherwise, draw directly to the Printable. Then the intentions of your output operations are retained and the print driver can produce optimal quality. I'm sure with a bit a thought you can refactor your code such that you still have a very maintainable structure.

BufferedImage.setRGB pixel wrong color

Trying to do something relatively simple, given a 512x512 png of a map, I'm attempting to plot points. My code's fairly straightforward, I've tried using both the setRGB function and the Graphics2D object that is returned by the createGraphics function. I must be overlooking something simple. EDIT: I should mention that I'm not looking to create a new BufferedImage, I'm looking to modify the existing BufferedImage, since successive library calls will continue to modify the BufferedImage that I'm working with. (In the example code below, I read the BufferedImage from a file, for a simple way to replicate the issue.
File outputImage = new File("before.png");
BufferedImage img = ImageIO.read(outputImage);
img.setRGB(255, 255, new Color(0f, 1f, 0).getRGB());
File after = new File("after.png");
ImageIO.write(img, "png", after);
If you zoom in on the resulting pixel, it's not green, but some darker grey. Since this behavior is uniform with the Graphics2D, I'm hoping solving this problem will address that as well.
The color space of the BufferedImage must be causing a problem.
In the code below I use your original image and paint it to a BufferedImage with the specified color space:
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
import java.io.*;
import javax.imageio.*;
import java.net.*;
public class SSCCE extends JPanel
{
SSCCE()
{
try
{
BufferedImage original = ImageIO.read( new File("map.png") );
int width = original.getWidth(null);
int height = original.getHeight(null);
int size = 100;
BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = bi.createGraphics();
g2d.drawImage(original, 0, 0, null);
int color = new Color(0f, 1f, 0f).getRGB();
bi.setRGB(10, 10, color);
bi.setRGB(10, 11, color);
bi.setRGB(11, 10, color);
bi.setRGB(11, 11, color);
add( new JLabel( new ImageIcon(bi) ) );
}
catch(Exception e2) {}
}
private static void createAndShowGUI()
{
JFrame frame = new JFrame("SSCCE");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new SSCCE());
frame.pack();
frame.setLocationByPlatform( true );
frame.setVisible( true );
}
public static void main(String[] args)
{
EventQueue.invokeLater( () -> createAndShowGUI() );
/*
EventQueue.invokeLater(new Runnable()
{
public void run()
{
createAndShowGUI();
}
});
*/
}
}
Here's an attempt to describe a few ways you could work around the problem, based on discussion in the comments:
The problem is that the original image is using an IndexColorModel (or a color map or "palette" if you like). There is no green color that exactly matches the color you specify, so instead the color model does a lookup to get the "closest" color to the one you specified (you may not agree to this color being the closest match, but it is given the algorithm used).
If you set the color to one matching the colors of the image, you can paint in that color. Try new Color(0.8f, 0.9019608f, 0.6392157f) or RGB value 0xffcce6a3 for the light green one. Use new Color(0.6392157f, 0.8f, 1f) or 0xffa3ccff for the light blue.
If you wonder how I found those values, here's the explanation. Assuming colorModel is an IndexColorModel, you can use:
int[] rgbs = new int[colorModel.getMapSize()];
colorModel.getRGBs(rgbs);
...to get the colors in the color map. Choosing one of these colors should always work.
Now, if your "library" (which you haven't disclosed much details about) is using a fixed palette for generating these images, you are good, and can use one of the colors I mentioned, or use the approach described to get the colors, and choose an appropriate one. If not, you need to dynamically find the best color. And if you're really out of luck, there might be no suitable color available at all (ie., your map tile is all ocean, and the only color available is sea blue, it will be impossible to plot a green dot). Then there's really no other way to solve this, than to modify the library.
A completely different approach, could be similar to #camickr's solution, where you temporarily convert the image to true color (BufferedImage.TYPE_INT_RGB or TYPE_3BYTE_BGR), paint your changes onto this temporary image, then paint that image back onto the original. The reason why this might work better, is that the composing mechanism will use a dither and a better color lookup algorithm. But you'll still have the same issue related to available colors as described in the previous paragraph.
Here's a code sample, using the warm yellow color, and the output:
Color color = new Color(0.89411765f, 0.5686275f, 0.019607844f);
int argb = color.getRGB();
Graphics2D g = image.createGraphics();
try {
g.setColor(color);
g.fillRect(10, 10, 50, 50);
}
finally {
g.dispose();
}

Java Image Scaling Quality Lost in JAR File

I am coding a program which has an entire JPanel zoomed in with a JScrollPane as a map, but I also want to give users a scaled & minimized view of the whole map in the corner of the JScrollPane. I am using BlueJ, & (in BlueJ, at least) I have basically gotten what I want. To scale down the map, I create a single BufferedImage, bi, that is the size of the mini-map, then take its Graphics object, g, convert it to a Graphics2D object, g2, then give g2 a single set of RenderingHints, through:
g2.setRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON));
From there, I use the Graphics.scale(double sx, double sy) method on g2 to set the scale so that upon calling the paint(Graphics g) method of the large map with g2 as its parameter, it will draw a scaled version of the large map on bi that is the exact size of the mini-map. This happens 20 times per second (somewhat arbitrary, at that speed I think it looks best. The mini-map reflects what the user is doing in real-time & I think it looks laggy if done fewer than 20 times per second.)
On my Macbook Pro, The resulting look of this code is:
There is no noticeable lag with this code when I run the program, which has led me to keep the picture re-sampling at 20 times per second, since there appears to be plenty of CPU for this. However, upon exporting my project to a JAR file, the resulting look is this:
There still is little to no lag with the program running, but what the heck happened? So long as I run the code in BlueJ, it works - but in a JAR file, the mini-map looks horrible.
For clarification, this is the map to be scaled (which, ironically, is being scaled when I'm placing it in this post):
And here are the two BlueJ & JAR file mini-maps, side-by-side:
What could be the cause of this?
When I run "java -version", this is the output:
java version "1.8.0_31"
Java(TM) SE Runtime Environment (build 1.8.0_31-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.31-b07, mixed mode)
Tl;dr:
When I run my code in BlueJ, the small picture looks good. After exporting to a JAR file, it looks bad. What could be causing this?
EDIT: I am now including an SSCCE to give a better idea of what I mean.
import javax.swing.*;
import java.awt.*;
import java.awt.image.*;
import javax.swing.border.*;
public class ScaleTest
{
int scrollSpeed = 16, imageSize = 200;
int rows = 40, columns = 40;
JFrame f1 = new JFrame("ScaleTest: Full Size");
JScrollPane scrollPane = new JScrollPane();
JPanel f1Panel = new JPanel(new GridLayout(rows,columns,1,1));
JFrame f2 = new JFrame("ScaleTest: Scaled");
JPanel f2Panel = new JPanel()
{
public void paintComponent(Graphics g)
{
Graphics2D g2 = (Graphics2D)g;
g2.drawImage(bi,0,0,null);
}
};
BufferedImage bi = new BufferedImage(imageSize,imageSize,BufferedImage.TYPE_INT_ARGB);
public ScaleTest()
{
f1.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f1.add(scrollPane);
scrollPane.setPreferredSize(new Dimension(600,600));
scrollPane.getHorizontalScrollBar().setUnitIncrement(scrollSpeed);
scrollPane.getVerticalScrollBar().setUnitIncrement(scrollSpeed);
scrollPane.setViewportView(f1Panel);
for (int i = 0; i < rows*columns; i++)
{
JPanel p = new JPanel();
p.setBackground(Color.WHITE);
p.setBorder(BorderFactory.createLineBorder(Color.BLACK,1));
p.setPreferredSize(new Dimension(100,100));
f1Panel.add(p);
}
f1.pack();
f2.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f2.setResizable(false);
f2.add(f2Panel);
f2Panel.setPreferredSize(new Dimension(imageSize,imageSize));
f2.pack();
Graphics2D g2 = (Graphics2D)bi.getGraphics();
g2.setRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON));
g2.scale(((double)imageSize)/f1Panel.getWidth(),((double)imageSize)/f1Panel.getHeight());
f1Panel.paint(g2);
}
public static void main(String[] args)
{
EventQueue.invokeLater(new Runnable()
{
public void run()
{
createAndShowGUI();
}
});
}
public static void createAndShowGUI()
{
ScaleTest s = new ScaleTest();
s.f1.setVisible(true);
s.f2.setVisible(true);
}
}
In BlueJ the resulting scaled image is this:
Yet, in the JAR file that is produced from BlueJ, running the JAR produces this image:
What could be going on?
I would guess that your BlueJ using JDK1.6.0.
java version "1.6.0_45"
java version "1.8.0_31"
I found this answer on SE: https://stackoverflow.com/a/24746194/1695856 .
The upshot of it is that one way to improve the quality of the scaled down image is to blur it and scale it down in stages. In the following example I scaled the image down by 50% three times, and then by 80%, to get a final scale of 10% of the original.
The file referred to (map.png) was the large image you posted earlier. Setting the Antialiasing rendering hint is of no use when drawing scaled images as it only has an effect on lines, ovals, rectangles etc, but not images, from what I can tell. The Interpolation rendering hint helps a little, but relying on any rendering hint is not always going to work as they may not be implemented the same (or at all!) on different platforms.
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
public class Rescale {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
private ConvolveOp cop;
#Override
public void run() {
JFrame frame = new JFrame("Map");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
try {
BufferedImage im = ImageIO.read(new File("map.png"));
cop = new ConvolveOp(new Kernel(3, 3, new float[] { 0f, 1f / 5f, 0f, 1f / 5f, 1f / 5f, 1f / 5f, 0f, 1f / 5f, 0f }),
ConvolveOp.EDGE_NO_OP, null);
for (int i = 0; i < 3; i++) {
im = getScaled(im, 0.5);
}
im = getScaled(im, 0.8f);
JLabel lab = new JLabel(new ImageIcon(im));
frame.setContentPane(lab);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
} catch (IOException e) {
e.printStackTrace();
}
}
private BufferedImage getScaled(BufferedImage im, double scale) {
BufferedImage nim;
Dimension dim = new Dimension((int) (im.getWidth() * scale), (int) (im.getHeight() * scale));
nim = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) nim.getGraphics();
g.scale(scale, scale);
g.drawImage(im, cop, 0, 0);
g.dispose();
return nim;
}
});
}
}
I hope this helps.

Issues with drawing multiple circles (drawOval / Java)

My issues is the following: My actual project (of which the code below is a simplified version of) involves many concentric circles (each with a different colour) and animation utilising a Timer. The circles are drawn using the drawOval method.
My problem is that when these concentric circles are drawn, there appears to be loads of gaps in the outline of these circles, which I'm guessing is something to do with the fact that a circle is composed of pixels and lines as is any shape so the appearance of roundness is an illusion. I say this because when I swap the drawOval method for drawRect the painting looks as you would expect.
When messing around with other people's codes I saw that using RenderingHints somehow solved this problem however slowed down the animation beyond a point that I felt was acceptable.
Below is a screenshot of what is painted. Rather than seeing a solid opaque circle (as all of the circles drawn have the same colour in this example) we see this:
Here is my simplified code:
Test10
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.WindowConstants;
public class Test10 extends JPanel {
Circle[] circles;
public static void main(String[] args) {
new Test10().go();
}
void go() {
JFrame frame = new JFrame("Circle Test");
frame.getContentPane().add(this);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
circles = new Circle[200];
for (int i = 0; i < 200; i++) {
circles[i] = new Circle(i, ((2 * ( 200 - i) + 1)));
}
repaint();
frame.setPreferredSize(new Dimension(500,500));
frame.pack();
frame.setVisible(true);
}
public void paintComponent(Graphics g) {
for (Circle circle : circles ) {
circle.draw(g);
}
}
}
Circle
import java.awt.Graphics;
public class Circle {
int topLeft;
int diameter;
public Circle(int topLeft, int diameter) {
this.topLeft = topLeft;
this.diameter = diameter;
}
void draw(Graphics g) {
g.drawOval(topLeft, topLeft, diameter, diameter);
}
}
Could anyone explain to me a) Why this is happening and b) How to overcome this problem.
UPDATE
Having tried various methods including starting with the outermost circle and using fillOval instead of drawOval, and using a higher stroke value, I still find I have a problem with certain artefacts appearing similar to the screenshot Pavel posted. Here is a screenshot from my full application running the animation, if you look carefully you can see inconsistencies in the colour of mostly any given circle, resulting in these strange results. Their distribution actually follows the same pattern as the screenshot posted above so clearly something fundamental isn't being addressed by these options. Here is my screen shot:
It is impossible to draw perfect circle.
Try using the following method
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D)g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setStroke(new BasicStroke(2));
int i = 0;
for (Circle circle : circles ) {
Shape circle2 = new Ellipse2D.Double(i++, i, circle.diameter, circle.diameter);
g2d.draw(circle2);
}
}
You said you tried with RenderingHints, and it slowed your animation, but you haven't give us any code with animation, so maybe try my code (it would be good to see animation implementation). It looked better, but still not what you wanted. Setting stroke to another value will solve this (set to at least 2). Another one is to use .fill() instead of .draw(). I know that it is not perfect, but you may try it.
ANOTHER IDEA
I thought, that maybe you could add some blur to your image, so those artifacts are not visible?
I haven't done it before, but I found this (found HERE):
private class BlurGlass extends JComponent {
private JFrame f;
public BlurGlass(JFrame f) {
this.f = f;
setOpaque(false);
setFocusable(false);
}
public void paintComponent(Graphics g) {
int w = f.getWidth();
int h = f.getHeight();
setLocation(0, 0);
setSize(w, h);
g.setColor(new Color(0, 0, 0, 0.3f));
g.fillRect(0, 0, w, h);
}
}
now somwhere in go() method:
frame.setGlassPane(new BlurGlass(frame));
frame.getGlassPane().setVisible(true);
It looks a lot better for me. Play a bit with this GlassPane color (try changing .3f to some other value).
You might want to make the Stroke bigger. I've had luck with this in situations similar to yours
You can try by adding this line in your Circle class inside draw function:
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
//and draw the Oval on g2
Also another solution might be to fill the circles:
Ellipse2D.Double circle = new Ellipse2D.Double(x, y, diameter, diameter);
g2.fill(circle);
That happens because a computer cannot draw a perfect circle.
A computer uses square pixels to approximate a real circle but its just not possible to achieve perfection and that results in some pixels not being shown
Drawing a filled circle will help you
a detailed explanation
Can you please try fillOval method instead of drawOval.
g.fillOval(topLeft, topLeft, diameter, diameter);
Reverse your idea. Start with the outermost circle, then draw the inner circle and so on, finishing with the smallest circle. You should use fillOval in the process.
The other rendering hint that is often useful for circles/ovals is
g.setRenderingHint( RenderingHints. KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE);
See my other answer with more details and example.

Creating a sized image out of a string in Java

I am making a 2d engine using Java based on entities. The physics and sprites are done, but I still need to be able to draw text with the BaseText class. For experimental purposes I am using the following code in the Renderer class (that handles drawing all the sprites and such):
BufferGraphics.drawString(((BaseText) Entity).getText(), (int) -(Origin.getX() * PositionTransform), (int) -Origin.getY());
I would like to, however, be able to either move this code into the setText(final String Text) method of the BaseText entity, i.e. when it is called a new image is created containing the text specified (possibly in different fonts and sizes and such, I haven't decided).
My problem is this: I would like to be able to resize (scale) the text to my liking. It would also be nice to have the text converted to an image as I can get the dimensions of it and set the size of the text entity itself.
Basically, what I need follows something along these lines:
Take desired string and feed it into the setText method.
Take the string and draw it onto an image, sized so that the text will fit into it exactly.
Set this new image to the Image field in the entity so that the engine can draw it.
Is this even possible? There may be a way to do this with the FontMetrics class or whatever it may be called, but I'm not so sure as I have not used it before.
Edit : Let me clarify: I want to create a BufferedImage based on the size of some text set to a specific font and size, not size the text to fit an image.
Edit 2: Thanks to this fellow Andrew, whom so graciously provided code, I was able to add some code to the engine that, by all means, just plain should work. Again, however, not even with that drawRect in there, the image either remains either transparent or somehow is not getting drawn. Let me supply some breadcrumbs: -snip-
The stupid thing is that all the other sprites and images and such draw fine, so I am not sure how it could be the Renderer.
By the way, that was the paint() method.
Edit 3:
...
Uh...
...
Oh my.
I am...
...
Text can not explain how hard I belted myself in the face with my left palm.
BaseText.java
#Override
public BufferedImage getImage() {return null;}
Renderer.java
BufferedImage Image = Entity.getImage();
I am
a huge idiot.
Thank you, Andrew, for that code. It worked fine.
Edit 4: By the way, here's the final code that I used:
public void setText(final String Text)
{
Graphics2D Draw = (Graphics2D) Game.View.getBuffer().getDrawGraphics();
FontMetrics Metrics = Draw.getFontMetrics();
Rectangle2D Bounds = Metrics.getStringBounds(Text, Draw);
BufferedImage NewImage = new BufferedImage((int) Bounds.getWidth(), (int) (Bounds.getHeight() + Metrics.getDescent()), BufferedImage.TYPE_INT_RGB);
Draw = (Graphics2D) NewImage.getGraphics();
Draw.setColor(new Color(0xAAFF0000));
Draw.drawRect(0, 0, NewImage.getWidth(), NewImage.getHeight());
Draw.drawString(Text, 0, (int) Bounds.getHeight());
this.Image = NewImage;
this.Text = Text;
this.setSize(new Vector(NewImage.getWidth(), NewImage.getHeight()));
}
Use FontMetrics, GlyphView or the preferred size a JLabel (handy for getting the size needed to display formatted text.
Adjust the sizes of the font in step 1 until it fits. Call BufferedImage.createGraphics() to get a Graphics2D object. Paint the String to that.
I do not understand point 3, so won't comment.
Here is how it would work with either FontMetrics or a JLabel.
import java.awt.*;
import java.awt.image.*;
import java.awt.geom.Rectangle2D;
import javax.swing.*;
class TextSize {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// Technique 1 - FontMetrics
String s = "The quick brown fox jumps over the lazy dog!";
BufferedImage bi = new BufferedImage(
1,
1,
BufferedImage.TYPE_INT_RGB);
Graphics g = bi.getGraphics();
FontMetrics fm = g.getFontMetrics();
Rectangle2D b = fm.getStringBounds(s,g);
System.out.println(b);
bi = new BufferedImage(
(int)b.getWidth(),
(int)(b.getHeight() + fm.getDescent()),
BufferedImage.TYPE_INT_RGB);
g = bi.getGraphics();
g.drawString(s,0,(int)b.getHeight());
JOptionPane.showMessageDialog(
null,
new JLabel(new ImageIcon(bi)));
// Technique 3 - JLabel
JLabel l = new JLabel(s);
l.setSize(l.getPreferredSize());
bi = new BufferedImage(
l.getWidth(),
l.getHeight(),
BufferedImage.TYPE_INT_RGB);
g = bi.getGraphics();
g.setColor(Color.WHITE);
g.fillRect(0,0,400,100);
l.paint(g);
JOptionPane.showMessageDialog(
null,
new JLabel(new ImageIcon(bi)));
}
});
}
}

Categories

Resources