I have following image
Now I want to resize it to paint as background for a label, so that the borders still have the original size.
Here ist the code:
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.image.BufferedImage;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.border.EmptyBorder;
/**
* <code>BackgroundTest</code>.
*/
public class BackgroundTest extends JLabel {
// probably you need to download it and change the URL
private static final Image BACKGROUND = read(
"https://i.stack.imgur.com/MIwyR.png");
private static Image read(String url) {
try {
return ImageIO.read(new URL(url));
} catch (Exception e) {
throw new IllegalArgumentException("Cannot resolve image!", e);
}
}
#Override
protected void paintComponent(Graphics g) {
g.drawImage(scaleRect(BACKGROUND, new Insets(7, 7, 7, 7), getWidth(), getHeight()), 0, 0, this);
super.paintComponent(g);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(BackgroundTest::startUp);
}
private static void startUp() {
BackgroundTest label = new BackgroundTest();
label.setOpaque(false);
label.setText("<html>Simple multiline test to check<br>the painting</html>");
label.setBorder(new EmptyBorder(7, 7, 7, 7));
JFrame frm = new JFrame("Test");
JPanel panel = new JPanel();
panel.add(label);
panel.add(new JLabel(new ImageIcon(BACKGROUND)));
frm.add(panel);
frm.setSize(300, 300);
frm.setLocationRelativeTo(null);
frm.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frm.setVisible(true);
}
private Image scaleRect(Image src, Insets safePart, int width, int height) {
BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = result.createGraphics();
g.drawImage(src, 0, 0, width, height, this);
g.dispose();
return result;
}
}
The result looks ugly, because the borders are upscaled:
In this code example I only need a correct implementation of the function scaleRect, so that it considers the border insets (safePart parameter). The Graphics2D class has some other drawImage methods, but I don't know how to use them to achieve my goal.
You can take the 9-patch image approach - basically split your image into 9 pieces, some of which can be stretched without any visual artifacts and some that will stay at fixed size.
Basically cut it along these lines:
Then you can stretch:
top center & bottom center ones horizontally (if needed)
left center & right center ones vertically (if needed)
middle one can be stretched both horizontally and vertically
corners should remain in fixed sizes, otherwise you will get artifacts
Also you don't really need to cut the image, you can simply paint a specific part of the image:
public class SampleComponent extends JLabel
{
#Override
protected void paintComponent ( final Graphics g )
{
final Graphics2D g2d = ( Graphics2D ) g;
final Image image = ...; // Your image
g2d.drawImage (
image,
dstX1, dstY1, dstX2, dstY2,
imgX1, imgY1, imgX2, imgY2,
null
);
super.paintComponent ( g );
}
}
dst* - are the drawing destination rectangle coordinates
img* - are the coordinates of rectangle on your image
Note that this approach will only work with images that you can actually be split like shown above, it obviously won't work for something like photos or paintings, but for those cases I don't know why would you want to upscale them in the first place.
Many thanks to #MikleGarin for the idea. Here is the complete solution:
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.image.BufferedImage;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.border.EmptyBorder;
/**
* <code>BackgroundTest</code>.
*/
public class BackgroundTest extends JLabel {
// probably you need to download it and change the URL
private static final Image BACKGROUND = read(
"https://i.stack.imgur.com/MIwyR.png");
private static Image read(String url) {
try {
return ImageIO.read(new URL(url));
} catch (Exception e) {
throw new IllegalArgumentException("Cannot resolve image!", e);
}
}
#Override
protected void paintComponent(Graphics g) {
g.drawImage(scaleRect(BACKGROUND, new Insets(7, 7, 7, 7), getWidth(), getHeight()), 0, 0, this);
super.paintComponent(g);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(BackgroundTest::startUp);
}
private static void startUp() {
BackgroundTest label = new BackgroundTest();
label.setOpaque(false);
label.setText("<html>Simple multiline test to check<br>the painting</html>");
label.setBorder(new EmptyBorder(7, 7, 7, 7));
JFrame frm = new JFrame("Test");
JPanel panel = new JPanel();
panel.add(label);
panel.add(new JLabel(new ImageIcon(BACKGROUND)));
frm.add(panel);
frm.setSize(300, 300);
frm.setLocationRelativeTo(null);
frm.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frm.setVisible(true);
}
private Image scaleRect(Image src, Insets safePart, int width, int height) {
BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = result.createGraphics();
int srcWidth = src.getWidth(null);
int srcHeight = src.getHeight(null);
// top left
g.drawImage(src, 0, 0, safePart.left, safePart.right, 0, 0, safePart.left, safePart.top, null);
// top center
g.drawImage(src, safePart.left, 0, width - safePart.right, safePart.top, safePart.left, 0,
srcWidth - safePart.right, safePart.top, null);
// top right
g.drawImage(src, width - safePart.right, 0, width, safePart.top, srcWidth - safePart.right, 0,
srcWidth, safePart.top, null);
// center left
g.drawImage(src, 0, safePart.top, safePart.left, height - safePart.bottom, 0, safePart.top,
safePart.left, srcHeight - safePart.bottom, null);
// center
g.drawImage(src, safePart.left, safePart.top, width - safePart.right, height - safePart.bottom, safePart.left, safePart.top,
srcWidth - safePart.right, srcHeight - safePart.bottom, null);
// center right
g.drawImage(src, width - safePart.right, safePart.top, width, height - safePart.bottom, srcWidth - safePart.right, safePart.top,
srcWidth, srcHeight - safePart.bottom, null);
// bottom left
g.drawImage(src, 0, height - safePart.bottom, safePart.left, height, 0, srcHeight - safePart.bottom, safePart.left, srcHeight,
null);
// bottom middle
g.drawImage(src, safePart.left, height - safePart.bottom, width - safePart.right, height, safePart.left,
srcHeight - safePart.bottom, srcWidth - safePart.right, srcHeight, null);
// bottom right
g.drawImage(src, width - safePart.right, height - safePart.bottom, width, height, srcWidth - safePart.right,
srcHeight - safePart.bottom, srcWidth, srcHeight, null);
g.dispose();
return result;
}
}
Now it looks as it was required:
Related
I'm trying to develop a small rendering thing for a small project. I've gotten to a place where I can setup a Graphics2D object and call things from a simple render() loop, but when attempting to draw an image in place the image crops itself awkwardly and begins to lose data.
This is what I currently have in my draw function. Renderable is a pre-loaded BufferedImage.
public void draw(Graphics2D g2d, int x, int y, int scale, int rotation) {
scale = scale / 2;
AffineTransform at = new AffineTransform();
at.rotate(Math.toRadians(rotation), renderable.getWidth() / 2, renderable.getHeight() / 2);
AffineTransformOp op = new AffineTransformOp(at, AffineTransformOp.TYPE_BICUBIC);
BufferedImage copy = op.filter(renderable, null);
int pX = (400 - ((copy.getWidth() * scale) / 2)) + x;
int pY = (300 - ((copy.getHeight() * scale) / 2)) + y;
g2d.drawImage(copy, pX, pY, copy.getWidth() * scale, copy.getHeight() * scale, null);
}
When the rotation is not divisible by 90
When the rotation is divisible by 0, 90, 180, or 270
Is there something I am missing or should be doing differently? It appears to me that the image is losing data. The Graphics2D object passed into the function is the same one used to render the lines behind the checkered square and should cover the full Canvas, which is 800x600 pixels.
Update
int rotation = 0;
while (EngineGlue.isValid()) {
rotation = rotation + 15;
if (rotation >= 360) {
rotation = 0;
}
Graphics2D g2d = (Graphics2D)EngineGlue.getInstance().getCanvas().getBufferStrategy().getDrawGraphics();
g2d.clearRect(0, 0, 800, 600);
g2d.drawLine(0, 300, 800, 300);
g2d.drawLine(400, 0, 400, 600);
//ta1.draw(g2d, -100, 0, 2, rotation);
//ta2.draw(g2d, 100, 0, 3, -rotation);
ta3.draw(g2d, 0, 0, 1, rotation);
EngineGlue.getInstance().getCanvas().getBufferStrategy().show();
Thread.sleep(150);
}
frame = new JFrame(title);
canvas = new Canvas();
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.setResizable(false);
frame.setVisible(true);
frame.add(canvas);
frame.pack();
frame.setSize(EngineGlue.Dimension.WIDTH.getValue(), EngineGlue.Dimension.HEIGHT.getValue());
frame.setLocationRelativeTo(null);
canvas.setSize(EngineGlue.Dimension.WIDTH.getValue(), EngineGlue.Dimension.HEIGHT.getValue());
canvas.createBufferStrategy(3);
You can use an AffineTransform to rotate and scale the image:
import java.awt.*;
import java.util.*;
import javax.swing.*;
import java.io.*;
import javax.imageio.*;
import java.awt.geom.*;
import java.awt.image.*;
public class RotateAndScale extends JPanel
{
private Image image;
public RotateAndScale(Image image)
{
this.image = image;
}
#Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
// create the transform, note that the transformations happen
// in reversed order (so check them backwards)
AffineTransform at = new AffineTransform();
// 4. translate it to the center of the component
at.translate(getWidth() / 2, getHeight() / 2);
// 3. do the actual rotation
at.rotate(Math.toRadians(45));
// 2. scale the image
at.scale(0.5, 0.5);
// 1. translate the object to rotate around the center
at.translate(-image.getWidth(this) / 2, -image.getHeight(this) / 2);
// draw the image
Graphics2D g2d = (Graphics2D) g;
g2d.drawImage(image, at, null);
// continue drawing other stuff (non-transformed)
//...
}
private static void createAndShowGUI()
{
try
{
BufferedImage image = ImageIO.read(new File("splash.gif"));
JFrame frame = new JFrame("SSCCE");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add( new RotateAndScale(image));
frame.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
frame.setSize(400, 400);
frame.setLocationByPlatform( true );
frame.setVisible( true );
}
catch(Exception e) { System.out.println(e); }
}
public static void main(String[] args) throws Exception
{
java.awt.EventQueue.invokeLater( () -> createAndShowGUI() );
}
}
Above is code I found somewhere a long time ago.
If you just want to rotate an image, the easiest way is to rotate the graphics context rather than the image itself. This reads in an image of Mars so you'll have to substitute your own image.
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class RotateImage extends JPanel {
JFrame frame;
int width;
int height;
BufferedImage b = null;
String imageFile = "f:/redmarble.jpg";
public RotateImage() {
frame = new JFrame();
setPreferredSize(new Dimension(width, height));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(this);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new RotateImage().startup());
}
public Dimension getPreferredSize() {
return new Dimension(width, height);
}
public void startup() {
try {
b = ImageIO.read(new File(imageFile));
} catch (IOException ioe) {
ioe.printStackTrace();
}
// calculate the diagonal of the image to size the panel
// this allows all rotations to fit within the panel
double diag = Math.hypot(b.getHeight(), b.getWidth());
width = (int) diag;
height = (int) diag;
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
repaint();
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
if (b == null) {
return;
}
Graphics2D g2d = (Graphics2D) g.create();
// set the angle of rotation and the center of rotation to the middle of the
// panel - width and height are equal but I still like to reference them
// independently (could save debugging time in the future).
g2d.rotate(Math.toRadians(45), width/2, height/2);
// now draw the image, adjusting x,y starting point to ensure rotation
// about the center.
g2d.drawImage(b, (width-b.getWidth())/2, (height-b.getHeight())/2,null);
}
}
try
{
BufferedImage original = ImageIO.read(OriginalImage);
BufferedImage resized = new BufferedImage(width, null, original.getType());
Graphics2D g2 = resized.createGraphics();
g2.drawImage(original, 0, 0, width, null, null);
g2.dispose();
ImageIO.write(resized, format, resizedImage);
}
I want to increase image width only height automatic adjust by width pixel.
Use the method Image.getScaledInstance as it show in the example below
import java.awt.BorderLayout;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
public class ImageTest {
private static final int WIDTH_INCREMENT = 100;
public static void main(String[] args) throws Exception {
BufferedImage image = ImageIO.read(new File("D:\\DDownloads\\Download.png"));
int targetWidth = image.getWidth() + WIDTH_INCREMENT;
// compute height using the aspect ratio
int targetHeight = (int) ((double) targetWidth * image.getHeight()) / image.getWidth();
Image target = image.getScaledInstance(targetWidth, targetHeight, Image.SCALE_SMOOTH);
// also you can use -1 as new height
// image.getScaledInstance(targetWidth, -1, Image.SCALE_SMOOTH);
// in this case the method computes the correct width by himself
BufferedImage toWrite = new BufferedImage(
targetWidth, targetHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = toWrite.createGraphics();
g2.drawImage(target, 0, 0, targetWidth, targetHeight, null);
g2.dispose();
ImageIO.write(toWrite, "png", new File("D:\\\\DDownloads\\\\converted.png"));
// next code is to show the both original and converted image on the screen
// Use invokeLater ofr correct initialization of Swing components
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frm = new JFrame("Images");
frm.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frm.add(new JLabel(new ImageIcon(image)), BorderLayout.NORTH);
frm.add(new JLabel(new ImageIcon(target)), BorderLayout.SOUTH);
frm.pack();
frm.setLocationRelativeTo(null); // center the window
frm.setVisible(true);
}
});
}
}
I have this program:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
public class TestLine {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new TestLine().start();
}
});
}
private static void start() {
JFrame frame = new JFrame();
frame.setContentPane(new CarthPanel());
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
private static class CarthPanel extends JComponent {
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
Graphics2D gg = (Graphics2D) g;
int w = gg.getClipBounds().width;
int h = gg.getClipBounds().height;
System.out.println("(w,h)=(" + w + "," + h + ")");
gg.translate(w - 1, 0); // <<< when uncommenting both lines, mirroring applies
gg.scale(-1.0, 1.0); //
paintTest(gg, w, h);
}
private static void paintTest(Graphics2D g, int w, int h) {
// black background
g.setColor(Color.black);
g.fillRect(0, 0, w, h);
// colored corners
g.setColor(Color.RED);
g.drawLine(0, 0, 10, 0);
g.drawLine(0, 0, 0, 10);
g.setColor(Color.RED);
g.drawLine(0, 199, 10, 199);
g.drawLine(0, 199, 0, 189);
g.setColor(Color.CYAN);
g.drawLine(189, 0, 199, 0);
g.drawLine(199, 0, 199, 10);
g.setColor(Color.CYAN);
g.drawLine(189, 199, 199, 199);
g.drawLine(199, 199, 199, 189);
// yellow squares
g.setColor(Color.yellow);
g.drawRect(3, 3, 10, 10);
g.fillRect(186, 3, 11, 11);
g.fillRect(3, 186, 11, 11);
g.drawRect(186, 186, 10, 10);
String chars = "ABC";
g.setFont(Font.decode("Arial 72"));
Rectangle2D rect = g.getFontMetrics().getStringBounds(chars, g);
g.drawString(chars, (int) (200 - rect.getWidth()) / 2, (int) (200 - rect.getHeight()));
}
}
}
If you run this program once with two particular lines commented in and then once with the same lines commented out (see the code), then you get to see this:
[
If you didn't spot the problem, here's a zoomed pic:
One would expect the image to be perfectly mirrored. This is true for all strokes and hollow shapes (= drawXxx() methods). All filled shapes however (= fillXxx() methods) are drawn exactly one pixel to the left of where I expect them; e.g. the filled yellow rectangles, and you can also notice that the black background has shifted, as seen by the white line at the right border.
Is this a bug or is this intended? I suspect that it has something to do with the difference how "width" and "height" are being handled in drawXxx() and fillXxx() methods:
drawRect(x,y,w,h) results in a hollow rectangle with X-axis boundaries x and x+w, so the rectangle is w+1 pixels wide.
fillRect(x,y,w,h) results in a filled rectangle with X-axis boundaries x and x+w-1, so the rectangle is w pixels wide.
What am I missing?
Not an answer, rather a hypothesis - the "contains(x,y)" of a rectangle will be true for the sides which forms the (x,y) corner of the rect and false for the sides making the (maxx, maxy) corner. Demo:
Rectangle r=new Rectangle(0, 0, 10, 10);
System.out.println(r.contains(0, 5)); // true
System.out.println(r.contains(5, 0)); // true
System.out.println(r.contains(10, 5)); // false
System.out.println(r.contains(5, 10)); // false
The hypothesis is that fillRect will not consider the "max sides" as inside points to be filled.
To assess the hypothesis, I'd suggest you to try using a GeneralPath which defines the same rectangle and see it there is any difference in filling it. GeneralPath should be forbidden to make any assumption on "the right-top borders are out of the shape's interior"
It is a bug, it is not a feature :)
Surely it is not intended, the mirrored image should be perfect, there's no reason why it shouldn't.
There is a way to use BufferedImage to render on it normally, and then drawing this BufferedImage flipped, the only drawback is a little performance influence and not well looking text drawn using LCD subpixeling.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
public class TestLine {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new TestLine().start();
}
});
}
private static void start() {
JFrame frame = new JFrame();
frame.setContentPane(new CarthPanel());
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
private static class CarthPanel extends JComponent {
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
Graphics2D gg = (Graphics2D) g;
// System.out.println("(w,h)=(" + w + "," + h + ")");
gg.translate(getWidth(), 0); // <<< when uncommenting both lines, mirroring applies
gg.scale(-1.0, 1.0); //
BufferedImage img = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
try {
paintTest(img.createGraphics());
} catch (Exception e) {
e.printStackTrace();
}
g.drawImage(img, 0, 0, null);
}
private void paintTest(Graphics2D g) {
// black background
g.setColor(Color.black);
g.fillRect(0, 0, getWidth(), getHeight());
// colored corners
g.setColor(Color.RED);
g.drawLine(0, 0, 10, 0);
g.drawLine(0, 0, 0, 10);
g.setColor(Color.RED);
g.drawLine(0, 199, 10, 199);
g.drawLine(0, 199, 0, 189);
g.setColor(Color.CYAN);
g.drawLine(189, 0, 199, 0);
g.drawLine(199, 0, 199, 10);
g.setColor(Color.CYAN);
g.drawLine(189, 199, 199, 199);
g.drawLine(199, 199, 199, 189);
// yellow squares
g.setColor(Color.yellow);
g.drawRect(3, 3, 10, 10);
g.fillRect(186, 3, 11, 11);
g.fillRect(3, 186, 11, 11);
g.drawRect(186, 186, 10, 10);
String chars = "ABC";
g.setFont(Font.decode("Arial 72"));
Rectangle2D rect = g.getFontMetrics().getStringBounds(chars, g);
g.drawString(chars, (int) (200 - rect.getWidth()) / 2, (int) (200 - rect.getHeight()));
}
}
}
I am trying to visually center an arbitrary user-supplied string on a JPanel. I have read dozens of other similar questions and answers here on SO but haven't found any that directly address the problem I am having.
In the code sample below, getWidth() and getHeight() refer to the width and height of the JPanel on which I'm placing the text string. I have found that TextLayout.getBounds() does a very good job of telling me the size of a bounding rectangle that encloses the text. So, I figured that it would be relatively simple to center the text rectangle in the JPanel rectangle by calculating the x and y positions on the JPanel of the lower left corner of the text-bounding rectangle:
FontRenderContext context = g2d.getFontRenderContext();
messageTextFont = new Font("Arial", Font.BOLD, fontSize);
TextLayout txt = new TextLayout(messageText, messageTextFont, context);
Rectangle2D bounds = txt.getBounds();
xString = (int)((getWidth() - (int)bounds.getWidth()) / 2 );
yString = (int)((getHeight()/2) + (int)(bounds.getHeight()/2));
g2d.setFont(messageTextFont);
g2d.setColor(rxColor);
g2d.drawString(messageText, xString, yString);
This worked perfectly for strings which were all uppercase. However, when I started testing with strings that contained lowercase letters with descenders (like g, p, y), the text was no longer centered. The descenders on the lower case letters (the parts that extend below the baseline of the font) were being drawn too low on the JPanel to have the text appear to be centered.
That's when I discovered (thanks to SO) that the y parameter passed to drawString() specifies the baseline of the drawn text, not the lower bound. Thus, again with the help of SO, I realized that I needed to adjust the placement of the text by the length of the descenders in my string:
....
TextLayout txt = new TextLayout(messageText, messageTextFont, context);
Rectangle2D bounds = txt.getBounds();
int descent = (int)txt.getDescent();
xString = (int)((getWidth() - (int)bounds.getWidth()) / 2 );
yString = (int)((getHeight()/2) + (int)(bounds.getHeight()/2) - descent);
....
I tested this with strings heavy in lowercase letters like g, p, and y and it worked great! WooHoo! But....wait. Ugh. Now when I try with only uppercase letters, the text is way too HIGH on the JPanel to look centered.
That's when I discovered that TextLayout.getDescent() (and all the other getDescent() methods I have found for other classes) returns the maximum descent of the FONT not of the specific string. Thus, my uppercase string was being raised up to account for descenders that didn't even occur in that string.
What am I to do? If I don't adjust the y parameter for drawString() to account for descenders then lowercase strings with descenders are visually too low on the JPanel. If I do adjust the y parameter for drawString() to account for the descenders then strings which do not contain any characters with descenders are visually too high on the JPanel. There doesn't seem to be any way for me to determine where the baseline is in the text-bounding rectangle for a GIVEN string. Thus, I can't figure out exactly what y to pass to drawString().
Thanks for any help or suggestions.
While I muck about with TextLayout, you could just use the Graphics context's FontMetrics, for example...
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class LayoutText {
public static void main(String[] args) {
new LayoutText();
}
public LayoutText() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private String text;
public TestPane() {
text = "Along time ago, in a galaxy, far, far away";
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.RED);
g2d.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight());
g2d.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2);
Font font = new Font("Arial", Font.BOLD, 48);
g2d.setFont(font);
FontMetrics fm = g2d.getFontMetrics();
int x = ((getWidth() - fm.stringWidth(text)) / 2);
int y = ((getHeight() - fm.getHeight()) / 2) + fm.getAscent();
g2d.setColor(Color.BLACK);
g2d.drawString(text, x, y);
g2d.dispose();
}
}
}
Okay, after some fussing about...
Basically, text rendering occurs at the baseline, this makes the y position of the bounds usually appear above this point, making it look like the text is been painted above the y position
To overcome this, we need to add the font's ascent minus the font's descent to the y position...
For example...
FontRenderContext context = g2d.getFontRenderContext();
Font font = new Font("Arial", Font.BOLD, 48);
TextLayout txt = new TextLayout(text, font, context);
Rectangle2D bounds = txt.getBounds();
int x = (int) ((getWidth() - (int) bounds.getWidth()) / 2);
int y = (int) ((getHeight() - (bounds.getHeight() - txt.getDescent())) / 2);
y += txt.getAscent() - txt.getDescent();
... This is why I love rendering text by hand ...
Runnable example...
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class LayoutText {
public static void main(String[] args) {
new LayoutText();
}
public LayoutText() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private String text;
public TestPane() {
text = "Along time ago, in a galaxy, far, far away";
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.RED);
g2d.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight());
g2d.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2);
FontRenderContext context = g2d.getFontRenderContext();
Font font = new Font("Arial", Font.BOLD, 48);
TextLayout txt = new TextLayout(text, font, context);
Rectangle2D bounds = txt.getBounds();
int x = (int) ((getWidth() - (int) bounds.getWidth()) / 2);
int y = (int) ((getHeight() - (bounds.getHeight() - txt.getDescent())) / 2);
y += txt.getAscent() - txt.getDescent();
g2d.setFont(font);
g2d.setColor(Color.BLACK);
g2d.drawString(text, x, y);
g2d.setColor(Color.BLUE);
g2d.translate(x, y);
g2d.draw(bounds);
g2d.dispose();
}
}
}
Take a look at Working with Text APIs for more information...
Updated
As has already been suggested, you could use a GlyphVector...
Each word (Cat and Dog) is calculated separatly to demonstrate the differences
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class LayoutText {
public static void main(String[] args) {
new LayoutText();
}
public LayoutText() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private String text;
public TestPane() {
text = "A long time ago, in a galaxy, far, far away";
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.RED);
g2d.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight());
g2d.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2);
Font font = new Font("Arial", Font.BOLD, 48);
g2d.setFont(font);
FontRenderContext frc = g2d.getFontRenderContext();
GlyphVector gv = font.createGlyphVector(frc, "Cat");
Rectangle2D box = gv.getVisualBounds();
int x = 0;
int y = (int)(((getHeight() - box.getHeight()) / 2d) + (-box.getY()));
g2d.drawString("Cat", x, y);
x += box.getWidth();
gv = font.createGlyphVector(frc, "Dog");
box = gv.getVisualBounds();
y = (int)(((getHeight() - box.getHeight()) / 2d) + (-box.getY()));
g2d.drawString("Dog", x, y);
g2d.dispose();
}
}
}
I think this answer is the correct way to do it however I have had problems in the past with custom fonts and getting their bounds. In one project I had to resort to actually getting the outline of the font and using those bounds. This method is likely more memory intensive however it seems to be a surefire way for getting font bounds.
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Font font = new Font("Arial", Font.BOLD, 48);
String text = "Along time ago, in a galaxy, far, far away";
Shape outline = font.createGlyphVector(g.getFontMetrics().getFontRenderContext(), text).getOutline();
// the shape returned is located at the left side of the baseline, this means we need to re-align it to the top left corner. We also want to set it the the center of the screen while we are there
AffineTransform transform = AffineTransform.getTranslateInstance(
-outline.getBounds().getX() + getWidth()/2 - outline.getBounds().width / 2,
-outline.getBounds().getY() + getHeight()/2 - outline.getBounds().height / 2);
outline = transform.createTransformedShape(outline);
g2d.fill(outline);
}
Like I said before try to use the font metrics but if all else fails try this method out.
It's the original image:
I use java.awt.Graphics.fillRect(int x, int y, int width, int height) to add a coloured rectangle on the image.
Graphics imageGraphics = image.createGraphics();
Color color = new Color(0,0,0,100);
imageGraphics.setColor(color);
imageGraphics.fillRect(0, 0, 800, 600);
So the image has been inverted and looks like this:
After that,I want to clear the black transparent rectangle partly and show the original image.
imageGraphics.clearRect(100,100,100,100);
But the effect is like this:
What my requirement is:
I want to know why it doesn't work and is there any other way to realize it?
Remember, painting is destructive. It might be possible to use AlphaComposite to achieve this result, but a simpler solution might be to simple constructive a compound shape and paint that instead.
The following example creates two Rectangles, one been the area we want to fill and one been the area we want to show, the second is then subtracted from the first (to create the window) and then the result is painted on top of the image
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.Area;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class ShowArea {
public static void main(String[] args) {
new ShowArea();
}
public ShowArea() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private BufferedImage img;
public TestPane() {
try {
img = ImageIO.read(new File("sample.png"));
Rectangle bounds = new Rectangle(0, 0, img.getWidth(), img.getHeight());
Rectangle clip = new Rectangle(150, 10, 100, 100);
Area area = new Area(bounds);
area.subtract(new Area(clip));
Graphics2D g2d = img.createGraphics();
g2d.setColor(Color.BLACK);
g2d.setComposite(AlphaComposite.SrcOver.derive(0.5f));
g2d.fill(area);
g2d.dispose();
} catch (IOException ex) {
ex.printStackTrace();
}
}
#Override
public Dimension getPreferredSize() {
return img == null ? new Dimension(200, 200) : new Dimension(img.getWidth(), img.getHeight());
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
int x = (getWidth() - img.getWidth()) / 2;
int y = (getHeight() - img.getHeight()) / 2;
g2d.drawImage(img, x, y, this);
g2d.dispose();
}
}
}
If I were doing some kind of paint program, I would do this all within the paintComponent method or in some way that it didn't effect the original image, otherwise you've basic destroyed the image until you re-load it
Another solution might be to take a copy of the original area you want to keep and repaint it back on top after, for example...
img = ImageIO.read(new File("sample.png"));
// This is the portion of the image we want to save...
BufferedImage cutout = img.getSubimage(150, 10, 100, 100);
// This is the area we want to paint over...
Rectangle bounds = new Rectangle(0, 0, img.getWidth(), img.getHeight());
Graphics2D g2d = img.createGraphics();
g2d.setColor(Color.BLACK);
// Save the current Composite so we can reset it...
Composite comp = g2d.getComposite();
// Apply the composite and fill the area...
g2d.setComposite(AlphaComposite.SrcOver.derive(0.5f));
g2d.fill(area);
// Reset the composite
g2d.setComposite(comp);
// Draw the part of the image we saved previously...
g2d.drawImage(cutout, 150, 10, this);
g2d.dispose();
You cant do that the way you're trying to.
It can be done by creating a BufferedImage filled with Color(0,0,0,200), then in that image draw rectangle with color Color(0,0,0,200) and then apply it on image.
Remember, that drawing filledRectange is not a "undo operation"- it is written on pixels and it can't be undone.
EDIT
Icon imageIcon = new javax.swing.ImageIcon("image.jpg");
BufferedImage mask = new BufferedImage(imageIcon.getIconWidth(), imageIcon.getIconHeight(), BufferedImage.TYPE_4BYTE_ABGR);
BufferedImage image = new BufferedImage(imageIcon.getIconWidth(), imageIcon.getIconHeight(), BufferedImage.TYPE_4BYTE_ABGR);
imageIcon.paintIcon(null, image.getGraphics(), 0, 0);
Graphics maskGraphics = mask.getGraphics();
//drawing grey background
maskGraphics.setColor(new Color(0, 0, 0, 120));
maskGraphics.fillRect(0, 0, mask.getWidth(), mask.getHeight());
//drawing black frame
maskGraphics.setColor(new Color(0, 0, 0, 255));
maskGraphics.drawRect(99, 99, 301, 301);
//drawing original image window
maskGraphics.drawImage(image, 100, 100, 400, 400, 100, 100, 400, 400, null);
//apply mask on image
new ImageIcon(mask).paintIcon(null, image.getGraphics(), 0, 0);
//result presentation
label.setIcon(new ImageIcon(image));
Yes this is easily doable. The problem is that any draw operation is destructive, what you see is what you get, the information that was painted upon is lost.
Something like this where you store a subimage, do your paint operation then draw the subimage back on top.
public class Q23709070 {
public static void main(String[] args) {
JFrame frame = new JFrame();
Panel p = new Panel();
frame.getContentPane().add(p);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
static class Panel extends JPanel {
int w = 400;
int h = 400;
int x = 100;
int y = 100;
BufferedImage img;
BufferedImage subImg;
BufferedImage save;
public Panel() {
try {
img = ImageIO.read(getClass().getResourceAsStream("0qzCf.jpg"));
} catch (IOException e) {
}
subImg = img.getSubimage(x, y, w, h);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
Color g2dColor = g2d.getColor();
Color fillColor = new Color(0, 0, 0, 100);
g2d.drawImage(img, 0, 0, null);
g2d.setColor(fillColor);
g2d.fillRect(0, 0, img.getWidth(), img.getHeight());
g2d.drawImage(subImg, x, y, null);
g2d.setColor(g2dColor);
if (save == null) {
save = new BufferedImage(img.getWidth(), img.getHeight(),
img.getType());
this.paint(save.getGraphics());
try {
ImageIO.write(save, "jpg", new File("save.jpg"));
} catch (IOException e) {
}
}
}
#Override
#Transient
public Dimension getPreferredSize() {
return new Dimension(img.getWidth(), img.getHeight());
}
}
}
Rendering