Concepts / Scaling Images

Scaling Images

The Graphics class provides the methods that copy an image onto the screen or an off-screen canvas, most commonly from inside paintComponent() of a JComponent.

  • Programming

Predicting a Simple Image Draw

Java programs often need to show an image on screen. The question this article answers is how a single drawing statement decides where an image appears and how large it looks. The method that does the work is drawImage(), which belongs to the Graphics class. Rather than explain everything up front, we will start from the simplest form of the call, guess what it does, and then test that guess by changing the call one piece at a time.

What do you think happens?

Given the basic call g.drawImage(img, x, y, this), what do you think determines where the image lands and how big it appears? Write down your guess before reading on.

Where the Copying Actually Happens

An image in a Java program is not drawn by the image itself. A method belonging to the Graphics class copies the image onto a drawing surface. That surface is usually the screen, though it can also be an off-screen canvas. In practice this copying most often happens inside the paintComponent() method of a JComponent, where a Graphics object arrives as a parameter for you to use.

The flow of control runs like this. The paintComponent() method is the caller. It hands its Graphics object to the drawing statement, and the Graphics object carries out the copying onto whatever surface it is attached to. Your code decides the call and its arguments, while the copying itself is performed through the Graphics object.

java
Notes on the code
  • paintComponent receives the Graphics object g as a parameter
  • g is the object that performs the copying onto the component
  • this refers to the JComponent that contains this drawing code

Parameters of the Basic drawImage() Call

Each piece of the basic call has a single job. The first argument, g, is the Graphics object that does the drawing. The second, img, is the image being drawn. The x and y values give the position at which the image is placed. The final argument, this, is the image observer. The source describes the image observer as something required when an image is drawn, and it is passed as the final parameter. In the source's examples, this is the JComponent that contains the drawing code.

gGraphics object that doesthe drawingimgthe image being drawnx, yposition where the image isplacedthisimage observer
What does each argument in a drawImage() call contribute, and which one tells Java where on the component the image appears?

Notice what the call does not include. It does not return a new image, and it does not specify a size. It only asks for the image to be placed at a location on the drawing surface. The position argument is the one that answers the question of where on the component the image appears.

Adding Width and Height

The drawImage() method has useful variations. One lets you scale the image as it is drawn, so that it fills a width and height you specify. The form that does this inserts two size values after the position values. Compared with the basic call, the position is still set by x and y, but the image is no longer drawn at whatever size it has. Its drawn size is set by width and height.

java
Notes on the code
  • 10, 20 is the position, unchanged from the basic call
  • 200 is the width the image is scaled to
  • 25 is the height the image is scaled to
  • this is still the image observer, passed last
100 x 50original image size200 x 25drawn size from width andheightscaled as drawn
If the image is 100 by 50 and the call specifies width 200 and height 25, how does the drawn result differ in each dimension from the original, and why?

Working through the example: the width grows from 100 to 200, so the drawn image is stretched horizontally by a factor of two. The height shrinks from 50 to 25, so the drawn image is compressed vertically by a factor of one half. Each dimension is set independently by its own value, which is why the two changes can point in opposite directions.

What do you think happens?

If you keep the same position but change only the width from 200 to 400 in g.drawImage(img, 10, 20, 400, 25, this), what happens to the drawn size?

  • The drawn width becomes 400, and the height stays at 25

  • The image moves to the right while keeping its original size

  • Nothing changes because the position is the same

Reveal answer
Answer:

The drawn width becomes 400, and the height stays at 25

The width and height are separate arguments. Changing one changes only that dimension, and the position stays at 10, 20.

Trace the Call: Position, Then Size

Worked example

Tracing a scaled draw

Trace what happens when paintComponent() runs g.drawImage(img, 30, 40, 150, 75, this), where img is 100 by 50 pixels.

  1. 1
    Identify the Graphics object

    g is the Graphics object passed into paintComponent(). It performs the copying onto the component.

  2. 2
    Read the position

    The values 30 and 40 give the position. The image's placement point is at x = 30, y = 40 on the drawing surface.

  3. 3
    Read the size

    The values 150 and 75 are the width and height the image is scaled to as it is drawn. The original 100 by 50 size no longer determines the drawn size.

  4. 4
    Read the observer

    this is the final argument and the image observer, which is the JComponent containing the code.

Result

The image appears with its upper-left point at (30, 40) and is drawn 150 units wide and 75 units tall. Its width grew by a factor of 1.5 and its height grew by a factor of 1.5 relative to the original 100 by 50.

Selecting a Region with Eight Coordinates

The most detailed variant of drawImage() takes eight coordinates instead of a position and size. The source describes it as a set of destination coordinates followed by a set of source coordinates, and then the observer. The destination pair of corners describes where on the drawing surface the result goes. The source pair of corners describes which rectangle of the original image is taken.

The source coordinates are what matter most when you want only part of an image. They pick out a rectangle inside the original image, and only the pixels inside that rectangle are copied. The destination coordinates then determine the rectangle on the drawing surface that receives those pixels. Because the two rectangles are specified separately, the portion you take and the place you put it are independent choices.

sx1, sy1top-left corner inside theoriginal imagedx1, dy1top-left corner on thedrawing surfacesx2, sy2bottom-right corner insidethe original imagedx2, dy2bottom-right corner on thedrawing surfaceregion copied fromregion copied to
Which pixels of the source image are copied, and where do they land on the component, when the four source coordinates and four destination coordinates differ?
java
Notes on the code
  • 0, 0, 200, 100 are the destination corners on the component
  • 10, 10, 60, 40 are the source corners inside the image
  • this is the observer, still passed last

In this call the source rectangle is 50 units wide and 30 tall, taken from the region starting at (10, 10) in the original image. The destination rectangle is 200 units wide and 100 tall, starting at (0, 0) on the component. The source and destination rectangles do not need to match in size, so the selected portion is stretched to fill the destination.

Mistakes That Produce Unexpected Results

When a drawn image is not what you expected, the cause is usually one argument in one call. The list below describes common places where the result departs from the plan. Read each entry as a question to ask about your own code: at which argument did my expectation and the call stop agreeing?

  • Common mistake

    Expecting the image to keep its original size after adding width and height

    What it looks like
    g.drawImage(img, 10, 20, 200, 25, this) on a 100 by 50 image
    Why it fails

    The width and height values set the drawn size, so the image is scaled as it is drawn rather than kept at its original dimensions.

    Do this instead

    Check the width and height arguments first when the image looks stretched or squashed.

  • Common mistake

    Passing the position values where the size values belong

    What it looks like

    Writing the size as if it were a second position in the call

    Why it fails

    The scaled form inserts two size values after the position values, so each number's role depends on its place in the argument list.

    Do this instead

    Count the arguments: g, img, x, y, then width and height, then the observer.

  • Common mistake

    Forgetting the image observer or placing it in the wrong position

    Why it fails

    The source describes the image observer as required and passed as the final parameter.

    Do this instead

    Keep this as the last argument in every form of the call.

  • Common mistake

    Confusing the source rectangle with the destination rectangle in the eight-coordinate form

    What it looks like

    Using coordinates from the component where coordinates from the original image were needed

    Why it fails

    The source pair picks a region of the original image, while the destination pair picks a region on the drawing surface, so swapping them copies the wrong part of the image to the wrong place.

    Do this instead

    Label the four source values and four destination values before writing the call.

  • Blank result: check that the Graphics object g is the one passed into paintComponent() and that the drawing happens inside that method.
  • Offset result: check the x and y position values, since they alone decide where the upper-left placement point sits.
  • Distorted result: check the width and height values, since they decide the drawn size in each dimension independently.
  • Partial region wrong: check the source corners against the original image and the destination corners against the drawing surface.

Practice Tracing Your Own Call

EASY

A 100 by 50 image is drawn with g.drawImage(img, 5, 5, 100, 100, this). Describe the position and the drawn size of the image, and say in which dimension the image is stretched and in which it is unchanged. Then state which argument you would check first if the image appeared shifted to the right.

Hints
  • The position is the pair immediately after the image argument.
  • Compare the width and height values to the original 100 by 50 size.
  • The position arguments decide where the upper-left point sits on the component.
MEDIUM

Write an eight-coordinate drawImage() call that copies the 40 by 40 region starting at (20, 20) in the original image and draws it into the component's top-left 80 by 80 area. Label each of the four source values and the four destination values in a comment before you write the call.

Hints
  • The source corners are (20, 20) and (60, 60), since the region is 40 units on each side.
  • The destination corners are (0, 0) and (80, 80).

Key Takeaways

  • drawImage() copies an image onto a drawing surface through a Graphics object, most often inside paintComponent() of a JComponent.
  • In the basic call drawImage(img, x, y, this), the x and y values set the position, and the final argument is the image observer.
  • Adding width and height after the position scales the image as it is drawn, and each dimension is set independently of the other.
  • The eight-coordinate form takes destination corners and then source corners, so it selects a rectangle from the original image and places it on a separate destination rectangle.
  • When a result is unexpected, check the argument that controls that aspect: position for placement, width and height for size, and the source and destination corners for partial regions.