HƯỚNG DẪN JAVA: Image and Pixel Processing

Joe

Thành viên VIP
21/1/13
3,075
1,342
113
JavaDude.gif

Today I discuss with you about Image and Color Processing. A proverb says "a picture is more than thousand words". How true! Even Confucius had to concede:
If you tell me about it I'll forget it,
If you show it to me I'll remember it,
If you let me do it I'll understand it
So. I show you and let you do ;) so that you remember and understand what Image and Color are. First, let talk about a digital Image (or picture if you so will). What is a digital image?

Image.png
Pic.1

A digital image is a group of PIXELS (PIX for Picture and EL for Element) which are represented by a combination of 3 different colors: RED, GREEN and BLUE or RGB for short. If a pixel can be made opaque or transparent it is an ARGB, where A is the controlling element. To represent the RGB or ARGB a computer needs at least 3 bytes (RGB) or 4 bytes (ARGB). And each byte can be displayed in 2 hex digits: 0x00 - 0xFF. For examples:
PHP:
The color ORANGE or an ORANGE Pixel:
RGB:  0xFAA500
ARGB: 0x00FAA500 (fully or 100% transparent), 0x80FFFFFF (haft or 50% transparent), 0xFFFFFFFF (fully or 100% opaque)
As you see, with the ALPHA byte a Color (or Pixel) has 255 variable possibilities between total opaque to total transparent. Pic.2 shows you how an ARGB looks like:

RGBA_comp.png
Pic.2

Everything looks good. BUT why I have mentioned ARGB and RGBA? Well, the world is not a simpleton, but always a YING-YANG world. IBM computers usually work with BIG-ENDIAN and some others work with LITTLE ENDIAN. What is that, the endian? It's the byte-sequence to represent a number or an address. Examples:
PHP:
The color 0x80FAA500 (a word or 32 bits)
                 byte 0     byte 1    byte 2   byte 3
LITTLE ENDIAN:     80         FA        A5      00
BIG ENDIAN         00         A5        FA      80
For human, the left digit is always higher than the next right digit and LITTLE ENDIAN is for human easier to understand than BIG ENDIAN. Luckily is that JAVA uses LITTLE ENDIAN to store its data (in files).

PixelSamples32bppRGBA.png
ARGB

HexRGBAbits.png
RGBA

You can run this little program to see the ENDIAN of your computer:
Java:
public class MyEndian {
    public static void main(String... args) {
        System.out.println("->EndianOrder:"+java.nio.ByteOrder.nativeOrder());
    }
}
and the output from an INTEL PC will be:
Code:
->EndianOrder:LITTLE_ENDIAN
(will continue)
 
Sửa lần cuối:

Joe

Thành viên VIP
21/1/13
3,075
1,342
113
From the RGB or ARGB/RGBA structure it's obvious that the value of RGB (or ARGB/RGBA) can only be presented by an integer (or 4 bytes). If you are working with another OOPL than JAVA you have to be carefully to consider the ENDIAN because the interchange between bytes and integer is dependent on the endian. As said, JAVA stores and works with LITTLE ENDIAN. The conversion between 4 bytes and an integer is a straightforward work:
Java:
// from integer to byte[4]
int i = 0x010203;
byte[] b = new bytes[] { (byte)((i & 0xFF000000) >> 24), (byte)((i & 0xFF0000) >> 16), (byte)((i & 0xFF00) >> 8), (byte)(i & 0xFF) };
// from byte[4] to integer
int I = (b[0]&0xFF) << 24 + (b[1] &0xFF) << 16 + (b[2] & 0xFF) << 8 + b[3] & 0xFF;
You may wonder why I have to AND a byte with 0xFF, am I right? Well, JAVA doesn't have UNSIGNED byte, hence the term "byte & 0xFF" signifies an internal conversion of a byte to an int before the AND operation can be executed. Example:
Java:
byte b = 0xFF; // as - 1 from a range between -1....-127 0....127 or 0xFF....x81 x00....0x7F
int i = (int) b;  // i yields -1 or 0xFFFFFFFF
int j = (int)(b & 0xFF); // j = 255 = 0xFFFFFFFF & 0x000000FF = 0x000000FF
IT or Computer Science (CS) without Mathematics is like a flower without water. If you don't master the most basic mathematical knowledge you wither like a flower. CS works with binary system and knows only ONE operation: addition. The other operations (subtract, multiplication and division) are derived from Addition. Binary means 0 (zero) or 1 (ONE). Not zero is one and vice versa. In Math an opposite of a binary number can be represented by either the FIRST Complement or the SECOND Complement:
  1. First Complement or 1st Complement is the reverse of each digit. Example: 15 = b1111, the 1st complement is 0000.
  2. Second Complement or 2nd Complement is the addition of the 1st Complement with 1. Example: 15 = b1111 +b1 = b10000.
With the complements Sub, Mul and Div can be implemented. I don't go into details with the MUL/DIV binary operation but show you how the Sub works. The algorithm is as following:
  1. The subtractor is converted to the 2nd-Complement,
  2. The 2nd-Complement is added to the subtrahend
  3. Depend on the sign the result could exceed the digit number of subtractor and subtrahend. If so, it's an OVERFLOW and it can be ignored.
Example: 4-digit bits, the 1st digit is the sign (0: plus, 1: minus)
Java:
a = 0111      (+7)
b = 0010      (+2)
________________
a - b = 5 or 0101

1) b to 1st complement: 0010 becomes 1101
2) 1st complement to 2nd complement by add 1 or 1101 + 0001 = 1110
3) a + 2nd complement: 0111 + 1110 = (1)0101 = 5, overflow 1
Multiplication is the addition of the multiplicand by multiplicator times.
Division is the subtraction of the dividend to the divisor until the Subtraction result is 0 or less than the divisor. The number of subtraction is the quotient and the last subtraction result is the rest (e.g. 12 : 2 yields 6x subtraction and the last result is 0. Quotient = 6, Rest = 0).

In reality we rarely deal with binary operation, but the knowledge helps us to derive and to estimate an operation. Addition is the fastest, then Subtraction, then Multiplication and the slowest is Division.

Back to an Image and Pixel.
In the past we have to take an image on a film and develop the image into a picture. The image is the negative, and the Picture is the final result. Similar to that, a digital image can be easily "reversed" into a negative by complementing the pixels. A Pixel is an integer or a 4 bytes value. To "negative" a Pixel we have to partition it into 4 bytes as I showed you above and then complement each byte with 255 (or 0xFF):
Java:
int cyan = 0x00FFFF;
int red  = (cyan & 0xFF0000) >> 16;
int green = (cyan & 0xFF00) >> 8;
int blue = cyan & 0xFF
// negative of cyan
int negCyan = ((0xFF - red) << 16) | ((0xFF - green) << 8) | (0xFF -blue);
And every image is consisted by width * height pixels (see Pic.1 of the 1st session. The width is the X axis, the height is the Y axis). The negative of an image is then:
Java:
    BufferedImage img = ImageIO.read(new File(imgFile));
    int width  = img.getWidth();
    int height = img.getHeight();
    for(int y = 0; y < height; ++y)
    for(int x = 0; x < width; ++x) {
      int p = img.getRGB(x,y); // get the pixel at location x, y
      // negate RGB by subtraction from 255
      int a = p & 0xFF000000;
      int r = 255 - (p >> 16) & 0xFF;
      int g = 255 - (p >> 8)  & 0xFF;
      int b = 255 - p & 0xFF;
      //set new RGB value
     img.setRGB(x, y,  a | (r << 16) | (g << 8) | b);
    }
negative.png
(will continue)
 
Sửa lần cuối:

Joe

Thành viên VIP
21/1/13
3,075
1,342
113
I have always mentioned that a good developer is the one who knows how to write the most efficient codes, not the one who writes the most beautiful codes. CS is mathematics. Mathematics can be very slow or very fast. Because CS is binary mathematics all CS operations are binary-oriented. Human mathematics bases on the decimal system. If you think as a human on working with computer your work becomes insufficient and erroneous. Even when we do some math we spend more time with Multiplication and Division than with Addition or Subtraction. And if we see two things that look equal we conclude that they are equal without having to do a subtraction in order to find out whether they are equal (result 0) or not. Such a conclusion is a logical reasoning. A logical reasoning is usually much faster than a subtraction. Similar to that logical operations on computer are much faster than mathematical operations. Let compare the following lines:
Java:
// logical operation (hard to understand):
p = (a << 24) | (r << 16) | (g << 8) | b;
// mathematical operation (easy to understand)
p = (a * 0x1000000) + (r * 0x10000) + (g * 0x100) + b;
Both yield the same result, but the logical operation is much faster than the mathematical operation. If you don't believe me you could consult, for example, the Clock cycles per Instruction of SHIFT-LEFT ( << ) and MUL ( * ), SHIFT-RIGHT ( >> ) or DIV ( : ), OR ( | ) and ADD ( + ) of Intel or AMD.

In many cases you can replace a math. operation with a logical one. For example: a * (2 ** n) can be replaced with a << n or a / (2 * *n) with a >> n. Because an image usually consists of thousands or millions of pixels (mega pixels) a loop with millions of logical operations is perceivably faster than the same loop with math. operations. An example with the Bitcoin.png or BigOnion.jpg:
Java:
public class Pixels {
  public static void main(String... a) throws Exception {
    BufferedImage img = ImageIO.read(new File(a[0]));
    int width  =  img.getWidth();
    int height = img.getHeight();
    System.out.println(a[0]+" has a Width:"+width+", a Height:"+height+" and "+(width*height)+" pixels");
  }
}
and the output
Code:
C:\JFX\Color>javac -g:none -d ./classes Pixels.java

C:\JFX\Color>java Pixels ./images/Bitcoin.png
./images/Bitcoin.png has a Width:467, a Height:376 and 175592 pixels

C:\JFX\Color>java Pixels ./images/BigOnion.jpg
./images/BigOnion.jpg has a Width:850, a Height:566 and 481100 pixels

C:\JFX\Color>
To process a loop of 175 K or 480 K pixels it is the matter of efficiency, not of beauty.

I have to tell you a hilarious story. In 2014 I had interviewed two applicants for 2 jobs as Senior Developer (who was responsible for a project) and as Developer. What I didn't know that the two came from the same company and one of them was the boss of the other. I raised a question about the best way to process the pixels of an image. The ex-boss was proud of his 5 years experience while the other had only 3 years of experience. The ex-boss gave me the solution with "p = (a * 0x1000000) + (r * 0x10000) + (g * 0x100) +b" and argued about the readability for the laymen, while the other came with "p = (a << 24) | (r << 16) | (g << 8) | b" and argued that the response time (performance) was more important than the beauty and the users usually wouldn't care about the beauty of the codes. The senior and project leader job went to the one with "less experience", but more knowledge about Computer Science. On the day they came to work I perceived that something went wrong with the two. A week later I got the immediate resignation of the ex-boss without any reason, even the paying was better than his ex job. And then the new senior developer came to me and told me about the emerging problems between him and his ex-boss who couldn't stand with the reality that his ex-subordinate was now his boss.

The old story is still actual and you, as user, still confront the sluggishness of scrolling an online paper full of images. Probably because the "developers" were keener of beautiful than efficient codes, and at the expense of the viewers (or users).

With the X-Y coordinates we can address any pixel within an image and modify it at our will. For example, we want to change the color of a string on the picture from black to cyan. The image is "HelloWorld.png" with the string "Hello World and Joe" in black.
Java:
    BufferedImage inImg = ImageIO.read(new File(inFile));
    int width  = (int) inImg.getWidth();
    int height = (int) inImg.getHeight();
    //
    int RED   = (0xFF0000 & old_color) >> 16;
    int GREEN = (0xFF00 & old_color) >> 8;
    int BLUE  = (0xFF & old_color);
    //
    int RED_n   = (0xFF0000 & new_color) >> 16;
    int GREEN_n = (0xFF00 & new_color) >> 8;
    int BLUE_n  = (0xFF & new_color);
    //
    int[] pixels = inImg.getRGB(0, 0, width, height, null, 0, width);
    for (int y = 0, i = 0; y < height; ++y)
    for (int x = 0; x < width; ++x) {
      // a Pixel is an ARGB
      int alpha = pixels[i] & 0xFF000000;
      int red   = (pixels[i] >> 16) & 0xFF;
      int green = (pixels[i] >> 8) & 0xFF;
      int blue  =  pixels[i++] & 0xFF;
      if (RED == red && GREEN == green && BLUE == blue) {
        red   = RED_n;
        green = GREEN_n;
        blue  = BLUE_n;
      }
      inImg.setRGB(x, y, alpha | red << 16 | green << 8 | blue);
    }
1639917593910.png

(will continue)
 
Sửa lần cuối:

Joe

Thành viên VIP
21/1/13
3,075
1,342
113
With the partition of a PIXEL into 3 color elements RED, GREEN and BLUE we can (theoretically) manipulate one of the three or all. The ReplaceColor loop can be optimized as following if all 3 color elements are to be replaced:
Java:
    BufferedImage inImg = ImageIO.read(new File(inFile));
    int width  = (int) inImg.getWidth();
    int height = (int) inImg.getHeight();
    //
    int oldRGB = 0x00FFFFFF & old_color; // ignore the ALPHA
    int newRGB = 0x00FFFFFF & new_color; // ignore the ALPHA
    //
    int[] pixels = inImg.getRGB(0, 0, width, height, null, 0, width);
    for (int y = 0, i = 0; y < height; ++y)
    for (int x = 0; x < width; ++x) {
      // a Pixel is an ARGB
      int alpha = pixels[i] & 0xFF000000;
      int rgb   = pixels[i++] & 0x00FFFFFF;
      if (oldRGB == rgb) rgb = newRGB;
      inImg.setRGB(x, y, alpha | rgb);
    }
The loop is now neater and more efficient than the original loop. Suppose that we want to change only the GREEN element of all Pixels (or all RGB) then we have to dive into the RGB elements:
Java:
    // replace only the GREEN element
    BufferedImage inImg = ImageIO.read(new File(imgFile));
    int width  = (int) inImg.getWidth();
    int height = (int) inImg.getHeight();
    //
    int GREEN   = 0xFF00 & old_color;
    int GREEN_n = 0xFF00 & new_color;
    //
    int[] pixels = inImg.getRGB(0, 0, width, height, null, 0, width);
    for (int y = 0, i = 0; y < height; ++y)
    for (int x = 0; x < width; ++x, ++i) {
      // a Pixel is an ARGB
      int rgb   = pixels[i] & 0xFFFF00FF;
      int green = pixels[i] & 0xFF00;
      if (GREEN == green) green = GREEN_n;
      inImg.setRGB(x, y, rgb | green);
    }
If we want to color an image in only ONE color out of the three elements we need only to turn OFF the unneeded color elements (here: RED and BLUE to achieve the GREEN image).
Java:
    BufferedImage img = ImageIO.read(new File(imgFile));
    int width  = img.getWidth();
    int height = img.getHeight();
    //convert to greenscale
    for(int y = 0; y < height; ++y)
    for(int x = 0; x < width; ++x) {
      img.setRGB(x, y, img.getRGB(x,y) & 0xFF00FF00); // turn off RED and BLUE
    }
Bild_2021-12-20_110722.png

Similar to the color elements we can change the ALPHA value of any specified pixel, too. The ALPHA value determines the opaqueness (100% by 0xFF) or the translucency (or transparency - 100% by 0x00). Example:
Java:
    BufferedImage img = ImageIO.read(new File(imgFile));
    int width  = img.getWidth();
    int height = img.getHeight();
    //
    BufferedImage bImg = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
    int RGB   = pixel & 0x00FFFFFF;  // 100% translucent
    int[] pixels  = img.getRGB(0, 0, width, height, null, 0, width);
    for(int y = 0, i = 0; y < height; y++)
    for(int x = 0, l = 0; x < width; ++x, ++i) {
      int rgb   = pixels[i] & 0x00FFFFFF;
      if (rgb == RGB) bImg.setRGB(x, y, rgb); // 100% transparent
      else bImg.setRGB(x, y,  pixels[i]); // let it be
    }
The invocation:
Java:
// all BLACK pixels: 0x000000, tolerant +/- 0x10, source: FataMorgana.png
bImg = ImageTools.transparentColor(ImageTools.toInt(0x000000, 0x10, "FataMorgana.png");
ALL BLACK pixels are now so translucent that the background color (vary via the Slider: here is light blue) becomes visible.

1640175972442.png

(will continue)
 
Sửa lần cuối:

Joe

Thành viên VIP
21/1/13
3,075
1,342
113
There are two possibilities to process a Pixel or RGB/ARGB/RGBA:
  1. Via a Pixel-Array (or int[ ])
  2. Via a Direct Pixel Access
Both achieve the same result. It depends fully on the working environment of your application. An element of Pixel-Array can be quickly accessed, but it requires in advance a huge memory in reserve. Imagine that an image has 4 Mega Pixels and each Pixel is an int (4 bytes). That means the app eats up 16 MB. Quite a lot, isn't it? All the examples above work with Pixel array. The way to work with Direct Pixel Access can be implemented, for example, as following:
Java:
    BufferedImage img = ImageIO.read(new File(parms[1]));
    int width  = img.getWidth();
    int height = img.getHeight();
    //
    BufferedImage bImg = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
    int RGB   = pixel & 0x00FFFFFF;  // 100% translucent

    /* Pixel Array
    int[] pixels  = img.getRGB(0, 0, width, height, null, 0, width);
    for(int y = 0, i = 0; y < height; y++)
    for(int x = 0, l = 0; x < width; ++x, ++i) {
      int rgb   = pixels[i] & 0x00FFFFFF;
      if (rgb == RGB) bImg.setRGB(x, y, rgb); // 100% transparent
      else bImg.setRGB(x, y,  pixels[i]); // let it be
    }
    */

    // Direct Pixel access
    for(int y = 0; y < height; y++)
    for(int x = 0, l = 0; x < width; ++x) {
      int pixel = img.getRGB(x, y);
      int rgb   = pixel & 0x00FFFFFF;
      if (rgb == RGB) bImg.setRGB(x, y, rgb); // 100% transparent
      else bImg.setRGB(x, y,  pixel); // let it be
    }
You may wonder where is the (dis)advantage of Direct Pixel Access, am I right? Well, the advantage is the request of memory on demand and the disadvantage is the high accessing activity between the BufferedImage and the processing loop. In essence:
Java:
// Pixel Array
int[] pixels  = img.getRGB(0, 0, width, height, null, 0, width); // ALL at a Time
// Direct Pixel Access
int pixel = img.get(x, y); // ONE by ONE
So far and so good. What's about JavaFX? Java Image (SWING and AWT) is TOTALLY INCOMPATIBLE with JavaFX counterpart. But: where there's an effort it is there a solution. The commonness between JavaFX and "normal" Java is the IO/NIO. SWING and AWT require the Image object java.awt.Image while JavaFX demands javafx.scene.image.Image. What you have to do is to bring the awt.Image into a buffer (e.g. java.awt.image.BufferedImage) and then use the buffer as an INPUT to create a JavaFX Image. Example:
Java:
  /**
  @param color int 6-digits hex-String beginning with 0x.... for the to be made transparent<br>
  @param imgFile String  Image file name
  @exception Exception thrown by JAVA
  */
  public static BufferedImage transparentColor(int color, String imgFile) throws Exception {
    BufferedImage img = ImageIO.read(new File(imgFile));
    int width  = img.getWidth();
    int height = img.getHeight();
    // Create a BufferImage with 4 bytes ARGB format
    BufferedImage bImg = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
    int RGB   = color & 0x00FFFFFF;
    for(int y = 0; y < height; y++)
    for(int x = 0; x < width; ++x) {
      int p = img.getRGB(x,y);
      int rgb = p & 0x00FFFFFF;
      if (rgb == RGB) bImg.setRGB(x, y, rgb); // 100% transparent
      else bImg.setRGB(x, y, p); // let it be
    }
    return bImg;
  }
  // JavaFX: return a JavaFX Image
  public static javafx.scene.image.Image transparentJFXColor(int color, String imgFile) throws Exception {
    ByteArrayOutputStream bao = new ByteArrayOutputStream();
    ImageIO.write(transparentColor(color, imgFile), suffix(imgFile), bao);
    return new javafx.scene.image.Image(new ByteArrayInputStream(bao.toByteArray()));
  }
  ...
  private static String suffix(String file) {
    int p = file.lastIndexOf(".");
    if (p > 0) return file.substring(p+1);
    return "png";
  }
The awt.image.BufferedImage is buffered in ByteArrayOutputStream and then its content is used as the data of an INPUT STREAM (ByteArrayInputStream) to instantiate JavaFX Image.

The reverse way from JavaFX to SWING/AWT Image is a bit simpler, but more cumbersome. Example:
Java:
    javafx.scene.image.Image image = new javafx.scene.image.Image(imgFile);
    //
    int width = (int)image.getWidth();
    int height = (int)image.getHeight();
    byte[] buf = new byte[width * height * 4]; // 4 for ARGB
    // ARGB instance from PixelReader
    javafx.scene.image.PixelReader reader = image.getPixelReader();
    reader.getPixels(0, 0, w, h, PixelFormat.getIntArgbInstance(), buf, 0, w * 4); // load the buffer
    // save the buffer in ByteArrayInputStream  and use it as INPUT STREAM for ImageIO
    java.io.ByteArrayInputStream bai = new java.io.ByteArrayInputStream(buf);
    // convert to BufferedImage for SWING/AWT
    java.awt.image.BufferedImage = javax.imageio.ImageIO.read(bai);
Let try to scale the RED element in JavaFX by turn-off of GREEN and BLUE. The codes:
Java:
public  static javafx.scene.image.Image  scalingJFX(int pattern, String imgFile) throws Exception {
    BufferedImage img = ImageIO.read(new File(imgFile));
    int width  = img.getWidth();
    int height = img.getHeight();
    //make sure that the ALPHA element is kept
    pattern |= 0xFF000000; // keep ALPHA
    for(int y = 0; y < height; ++y)
    for(int x = 0; x < width; ++x) {
      img.setRGB(x, y, img.getRGB(x,y) & pattern);
    }
    ByteArrayOutputStream bao = new ByteArrayOutputStream();
    ImageIO.write(scaling(pattern, imgFile), suffix(imgFile), bao);
    return new javafx.scene.image.Image(new ByteArrayInputStream(bao.toByteArray()));
}
the invocation with a pattern in 6 hex: 0xFF0000
Java:
redView = new ImageView(ImageTool.scalingJFX(0xFF0000, imgFile)); // Keep RED and turn off GREEN and BLUE
Bild_2021-12-21_101513.png

(will continue)
 
Sửa lần cuối:

Joe

Thành viên VIP
21/1/13
3,075
1,342
113
We are now able to control and to manipulate the pixels of an image. The logical operations like AND ( & ), OR ( | ), etc. allow us to mix, to merge images (plural) or even to copy a piece of image. Example: merge a piece of Image A into image B
Java:
    BufferedImage img_S = ImageIO.read(new File(source));
    int height = img_S.getHeight();
    int width  = img_S.getWidth();
    int wi = sourceX + xLen, hi = sourceY + yLen;
    //
    BufferedImage img_D = ImageIO.read(new File(dest));
    height = img_D.getHeight();
    width  = img_D.getWidth();
    //
    for (int y = destY, i = sourceY; y < height && i < hi; ++y, ++i) {
      for (int x = destX, j = ssourceX; x < width && j < wi; ++x, ++j) {
        int RGB = img_D.getRGB(x, y) | img_S.getRGB(j, i); // merge the two
        img_D.setRGB(x, y, RGB); // replace the original with the merged
      }
    }
    return img_D;
The outcome of the merge between FataMorgana.png and BigOnion.png
Bild_2021-12-22_100212.png

To overlay an image with another (piece of) image we simply replace the destination pixels with the source pixels:
Java:
img_D.setRGB(x, y, img_S.getRGB(j, i));
The OCR (Optical Character Recognition) technology is, for example, bases on the recognition of pixels and an algorithm that defines some certain pixel positions of each character. To do that every character has to be converted into an image. For example or alphanumeric letters. The letters are defined by a FONT and its attributes (plain, bold, italic, etc.) The problem is how to create a Letter-Image. I show you hereunder a JAVA technique that allows you to convert any letter of any font into an image.
Java:
  public static BufferedImage createFontImage(String string, String fontName, int fontAtt, int size) {
    //  create a BufferImage with width = 1 and height = 1
    BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = image.createGraphics();
    // create a font with the given fontName (e.g. TimesRoman), attribute (e.g. Font.BOLD) and size (e.g. 15)
    Font font = new Font(fontName, fontAtt, size);
    // convert to FontMetrics
    FontMetrics metrics = g.getFontMetrics(font);
    int height = metrics.getHeight();
    int width  = metrics.stringWidth(string);
    // create an image with this width and height
    image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    // draw or write the given letter (or String) in BLACK with the background WHITE
    g = image.createGraphics();
    g.setFont(font);
    g.setColor(Color.WHITE);
    g.fillRect(0, 0, width, height);
    g.setColor(Color.BLACK);
    g.drawString(string, 0, height);
    //
    return image;
  }
You may wonder why I have to convert the Font into FontMetrics, right?
Font metrics are the measurements of characters in a particular font, which allow you to evenly space and uniformly align lines of text...
(more about Font metrics: click HERE) . I have to do that in order to get the Width and the Height of the given font. Example: letter H, TimesRoman, BOLD, 40

Bild_2021-12-22_110105.png

We got the Width 31 and the Height 52 (total: 6448 Pixels). But, as you have already noticed, the upper part of the image is "empty" (or void with WHITE) and that falsifies the true height of the H with Font TimesRoman, BOLD and 40 dpi (Dot Per Inch). So. The next step is to refine the H image in order to achieve the true Height of this letter.
Java:
    ...
    g = image.createGraphics();
    g.setFont(font);
    g.setColor(Color.WHITE);
    g.fillRect(0, 0, width, height);
    g.setColor(Color.BLACK);
    g.drawString(string, 0, height);
    //
    // eliminate the "void"
    //
    int y = 0;
    int white = Color.WHITE.getRGB();
    LOOP: for (; y < height; ++y)
    for (int x = 0; x < width; ++x)
    if (image.getRGB(x, y) != white) {
      if (y == 0) return image; // no need to rectify
      break LOOP;
    }
    int H = height - y;
    int[] pixels  = image.getRGB(0, y, width, H, null, 0, width);
    BufferedImage img = new BufferedImage(width, H, BufferedImage.TYPE_INT_ARGB);
    img.setRGB(0, 0, width, H, pixels, 0, width);
    return img;
And the result with less pixels than the original.

Bild_2021-12-22_112444.png

Hope you have enjoyed my tutorial about Image and Pixel Processing

Joe
UPDATE: if anyone has interest on this issue (ImageTools API, 2xSWING examples and 1 JavaFX example) (s)he can put a note with her/his email in my INBOX.
 
Sửa lần cuối: