Why are there these weird visual artifacts when scrolling my BufferedImage? And how do I prevent them from happening?

145 views Asked by At

I am creating an application that involves drawing a BufferedImage to a JComponent using Graphics2D in a paintComponent(Graphics) method.

I added the ability to zoom in and out of the image. The JComponent I am drawing to is contained within a JScrollPane. If the image becomes zoomed in enough, scrollbars appear.

Whenever I scroll the JScrollPane, I get weird visual artifacts.

Here is the zoomed out version of my image.

Zoomed out version of the image

Here is the zoomed in version of my image, with no visual artifacts.

Zoomed in version of the image

And here is what it looks like when I scroll down a bit, and then to the right.

Zoomed in version of the image with weird visual artifacts

Upon discovering this, I added a button that just triggers a repaint() and revalidate() call on the relevant JComponent. Doing that fixed it.

BEFORE PRESSING THE REPAINT BUTTON ON THE TOP RIGHT

Before pressing REPAINT

AFTER PRESSING THE REPAINT BUTTON ON THE TOP RIGHT

After pressing REPAINT

Here is the relevant portion of the code that handles painting the BufferedImage to the JComponent.

@Override
protected void paintComponent(final Graphics dontUse)
{

   super.paintComponent(dontUse);

   if (!(dontUse instanceof Graphics2D g))
   {
   
      throw new RuntimeException("Unknown graphics type = " + dontUse);
   
   }

   DRAW_DRAWN_PIXELS:
   {
   
      final Rectangle rectangle  = gui.drawingAreaScrollPane.getViewport().getViewRect();
   
      //We are only drawing a subsection because we may be working with GIGANTIC images.
      //If we attempt to draw the whole image, performance will drop like a rock.
      CALCULATE_SUBSECTION_TO_DRAW:
      {
      
         final int x = rectangle.x;
         final int y = rectangle.y;
         final int width = Math.min(rectangle.width, drawingArea.width);
         final int height = Math.min(rectangle.height, drawingArea.height);
      
         g.setBackground(gui.transparencyColor);
         g.clearRect(rectangle.x, rectangle.y, width, height);
      
      }
   
      DRAW_SUBSECTION_OF_IMAGE:
      {
      
         final int originalImageX;
         final int zoomedInImageX;
         final int originalImageY;
         final int zoomedInImageY;
      
         CALCULATE_ORIGINAL_POSITION:
         {
         
            final int quantizedX = quantize.applyAsInt(rectangle.x);
            final int quantizedY = quantize.applyAsInt(rectangle.y);
         
            zoomedInImageX = Math.max(quantizedX, 0);
            originalImageX = zoomedInImageX / gui.screenToImagePixelRatio;
         
            zoomedInImageY = Math.max(quantizedY, 0);
            originalImageY = zoomedInImageY / gui.screenToImagePixelRatio;
         
         }
      
         final int zoomedInImageWidth;
         final int originalImageWidth;
         final int zoomedInImageHeight;
         final int originalImageHeight;
      
         CALCULATE_ORIGINAL_DIMENSION:
         {
         
            final int minWidth = Math.min(rectangle.width, drawingArea.width);
            final int quantizedMinWidth = quantize.applyAsInt(minWidth);
            final int potentialWidth = quantizedMinWidth + gui.screenToImagePixelRatio;
            zoomedInImageWidth = Math.min(drawingArea.width, potentialWidth);
            originalImageWidth = zoomedInImageWidth / gui.screenToImagePixelRatio;
         
            final int minHeight = Math.min(rectangle.height, drawingArea.height);
            final int quantizedMinHeight = quantize.applyAsInt(minHeight);
            final int potentialHeight = quantizedMinHeight + gui.screenToImagePixelRatio;
            zoomedInImageHeight = Math.min(drawingArea.height, potentialHeight);
            originalImageHeight = zoomedInImageHeight / gui.screenToImagePixelRatio;
         
         }
      
         g.setPaint(gui.cursorColor);
      
         System.out.println(zoomedInImageX + " -- " + zoomedInImageY + " -- " + zoomedInImageWidth + " -- " + zoomedInImageHeight + " ---- " + originalImageX + " -- " + originalImageY + " -- " + originalImageWidth + " -- " + originalImageHeight + " ----- " + rectangle + " - " + drawingArea);
      
         g
            .drawImage
            (
               gui.image,
               zoomedInImageX,
               zoomedInImageY,
               zoomedInImageX + zoomedInImageWidth,
               zoomedInImageY + zoomedInImageHeight,
               originalImageX,
               originalImageY,
               originalImageX + originalImageWidth,
               originalImageY + originalImageHeight,
               //gui.transparencyColor,
               null
            )
            ;
      
      }
   
      DRAW_GRID_LINES:
      {
      
         if (gui.hasGridLines && gui.screenToImagePixelRatio > 1)
         {
         
            g.setPaint(gui.gridLinesColor);
            g.setStroke(new java.awt.BasicStroke(1));
         
            IntStream
               .range(rectangle.y, rectangle.y + rectangle.height)
               .forEach
               (
                  eachIndex ->
                  {
                  
                     if (eachIndex % gui.screenToImagePixelRatio == 0)
                     {
                     
                        g
                           .drawLine
                           (
                              rectangle.x,
                              eachIndex,
                              rectangle.x + rectangle.width,
                              eachIndex
                           )
                           ;
                     
                     }
                  
                  }
               )
               ;
         
            IntStream
               .rangeClosed(rectangle.x, rectangle.x + rectangle.width)
               .forEach
               (
                  eachIndex ->
                  {
                  
                     if (eachIndex % gui.screenToImagePixelRatio == 0)
                     {
                     
                        g
                           .drawLine
                           (
                              eachIndex,
                              rectangle.y,
                              eachIndex,
                              rectangle.y + rectangle.height
                           )
                           ;
                     
                     }
                  
                  }
               )
               ;
         
         }
      
      }
   
   }

   // gui.drawingAreaScrollPane.repaint();
   // gui.drawingAreaScrollPane.revalidate();

}

And here is a complete runnable example.


import javax.imageio.*;
import javax.imageio.event.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.colorchooser.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.File;
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.*;

public class GUI
{

   private static final Function<String, Border> TITLED_BORDER =
      title ->
         BorderFactory
            .createTitledBorder
            (
               null,
               title,
               TitledBorder.CENTER,
               TitledBorder.TOP
            )
            ;

   private static final Color CLEAR = new Color(0, 0, 0, 0);
   private static final Point OFF_SCREEN = new Point(-1, -1);

   public static final int ARBITRARY_VIEW_BUFFER = 200;
   private static final int MIN_PEN_SIZE = 1;
   private static final int MAX_PEN_SIZE = 10;
   private static final int MIN_SCREEN_TO_IMAGE_PIXEL_RATIO = 1;
   private static final int MAX_SCREEN_TO_IMAGE_PIXEL_RATIO = 30;
   private static final int DEFAULT_IMAGE_PIXEL_ROWS = 26;
   private static final int DEFAULT_IMAGE_PIXEL_COLUMNS = 24;

   private final JFrame frame;
   private final JScrollPane drawingAreaScrollPane = new JScrollPane();

   private BufferedImage image;
   private Color transparencyColor = Color.WHITE;
   private Color cursorColor = Color.BLACK;
   private Color gridLinesColor = Color.GRAY;
   private boolean hasGridLines = true;
   private int penSize = 1;
   private int screenToImagePixelRatio = 10;

   public static void main(final String[] args)
   {
   
      final BufferedImage image = new BufferedImage(20, 20, BufferedImage.TYPE_INT_ARGB);
   
      IntStream
         .range(0, 20)
         .forEach(i -> image.setRGB(i, i, Color.RED.getRGB()))
         ;
   
      new GUI(image);
   
   }

   public GUI()
   {
   
      this(DEFAULT_IMAGE_PIXEL_ROWS, DEFAULT_IMAGE_PIXEL_COLUMNS);
   
   }

   public GUI(final int numImagePixelRows, final int numImagePixelColumns)
   {
   
      this(new BufferedImage(numImagePixelColumns, numImagePixelRows, BufferedImage.TYPE_INT_ARGB));
   
   }

   public GUI(final BufferedImage image)
   {
   
      Objects.requireNonNull(image);
   
      try
      {
      
         UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
      
      }
      
      catch (Exception e)
      {
      
         throw new RuntimeException(e);
      
      }
   
      INITALIZING_METADATA_INSTANCE_FIELDS:
      {
      
         this.image = image;
      
      }
   
      this.frame = new JFrame();
   
      this.frame.setTitle("Paint");
      this.frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
   
      this.frame.add(this.createMainPanel());
   
      this.frame.pack();
      this.frame.setLocationByPlatform(true);
      this.frame.setVisible(true);
   
   }

   private JPanel createMainPanel()
   {
   
      final JPanel mainPanel = new JPanel(new BorderLayout());
   
      mainPanel.add(this.createCenterPanel(),   BorderLayout.CENTER);
   
      return mainPanel;
   
   }

   private JPanel createCenterPanel()
   {
   
      final GUI gui = this; //useful when trying to differentiate between different this'.
   
      final JPanel mainPanel;
   
      CREATE_MAIN_PANEL:
      {
      
         mainPanel = new JPanel();
         mainPanel.setLayout(new BorderLayout());
      
      }
   
      final JPanel drawingPanel;
   
      final Runnable UPDATE_DRAWING_PANEL_BORDER_TEXT;
      final Runnable RECREATE_DRAWING_AREA_FRESH;
      final Runnable REPAINT_DRAWING_PANEL;
   
      final BiFunction<Point, Integer, Point> originalToZoomed =
         (original, ratio) ->
            new Point(original.x * ratio, original.y * ratio)
            ;
   
      final BiFunction<Point, Integer, Point> zoomedToOriginal =
         (zoomed, ratio) ->
            new Point
            (
               (zoomed.x - (zoomed.x % ratio)) / ratio,
               (zoomed.y - (zoomed.y % ratio)) / ratio
            )
            ;
   
      CREATE_DRAWING_PANEL:
      {
      
         SET_UP_DRAWING_PANEL:
         {
         
            drawingPanel = new JPanel();
         
            drawingPanel.setLayout(new BoxLayout(drawingPanel, BoxLayout.PAGE_AXIS));
         
            drawingPanel.add(Box.createRigidArea(new Dimension(123, 456)));
         
         }
      
         REPAINT_DRAWING_PANEL =
            () ->
            {
            
               drawingPanel.repaint();
               drawingPanel.revalidate();
            
            }
            ;
      
         RECREATE_DRAWING_AREA_FRESH =
            () ->
            {
            
               final var thingToFocus = drawingPanel;
            
               drawingPanel.removeAll();
            
               final Dimension drawingArea = gui.deriveDrawingAreaDimensions();
            
               final IntUnaryOperator quantize =
                  num ->
                     num - (num % gui.screenToImagePixelRatio)
                     ;
            
               final Box.Filler box =
                  new Box.Filler(drawingArea, drawingArea, drawingArea)
                  {
                  
                     @Override
                     protected void paintComponent(final Graphics dontUse)
                     {
                     
                        super.paintComponent(dontUse);
                     
                        if (!(dontUse instanceof Graphics2D g))
                        {
                        
                           throw new RuntimeException("Unknown graphics type = " + dontUse);
                        
                        }
                     
                        DRAW_DRAWN_PIXELS:
                        {
                        
                           final Rectangle rectangle  = gui.drawingAreaScrollPane.getViewport().getViewRect();
                        
                           //We are only drawing a subsection because we may be working with GIGANTIC images.
                           //If we attempt to draw the whole image, performance will drop like a rock.
                           CALCULATE_SUBSECTION_TO_DRAW:
                           {
                           
                              final int x = rectangle.x;
                              final int y = rectangle.y;
                              final int width = Math.min(rectangle.width, drawingArea.width);
                              final int height = Math.min(rectangle.height, drawingArea.height);
                           
                              g.setBackground(gui.transparencyColor);
                              g.clearRect(rectangle.x, rectangle.y, width, height);
                           
                           }
                        
                           DRAW_SUBSECTION_OF_IMAGE:
                           {
                           
                              final int originalImageX;
                              final int zoomedInImageX;
                              final int originalImageY;
                              final int zoomedInImageY;
                           
                              CALCULATE_ORIGINAL_POSITION:
                              {
                              
                                 final int quantizedX = quantize.applyAsInt(rectangle.x);
                                 final int quantizedY = quantize.applyAsInt(rectangle.y);
                              
                                 zoomedInImageX = Math.max(quantizedX, 0);
                                 originalImageX = zoomedInImageX / gui.screenToImagePixelRatio;
                              
                                 zoomedInImageY = Math.max(quantizedY, 0);
                                 originalImageY = zoomedInImageY / gui.screenToImagePixelRatio;
                              
                              }
                           
                              final int zoomedInImageWidth;
                              final int originalImageWidth;
                              final int zoomedInImageHeight;
                              final int originalImageHeight;
                           
                              CALCULATE_ORIGINAL_DIMENSION:
                              {
                              
                                 final int minWidth = Math.min(rectangle.width, drawingArea.width);
                                 final int quantizedMinWidth = quantize.applyAsInt(minWidth);
                                 final int potentialWidth = quantizedMinWidth + gui.screenToImagePixelRatio;
                                 zoomedInImageWidth = Math.min(drawingArea.width, potentialWidth);
                                 originalImageWidth = zoomedInImageWidth / gui.screenToImagePixelRatio;
                              
                                 final int minHeight = Math.min(rectangle.height, drawingArea.height);
                                 final int quantizedMinHeight = quantize.applyAsInt(minHeight);
                                 final int potentialHeight = quantizedMinHeight + gui.screenToImagePixelRatio;
                                 zoomedInImageHeight = Math.min(drawingArea.height, potentialHeight);
                                 originalImageHeight = zoomedInImageHeight / gui.screenToImagePixelRatio;
                              
                              }
                           
                              g.setPaint(gui.cursorColor);
                           
                              System.out.println(zoomedInImageX + " -- " + zoomedInImageY + " -- " + zoomedInImageWidth + " -- " + zoomedInImageHeight + " ---- " + originalImageX + " -- " + originalImageY + " -- " + originalImageWidth + " -- " + originalImageHeight + " ----- " + rectangle + " - " + drawingArea);
                           
                              g
                                 .drawImage
                                 (
                                    gui.image,
                                    zoomedInImageX,
                                    zoomedInImageY,
                                    zoomedInImageX + zoomedInImageWidth,
                                    zoomedInImageY + zoomedInImageHeight,
                                    originalImageX,
                                    originalImageY,
                                    originalImageX + originalImageWidth,
                                    originalImageY + originalImageHeight,
                                    //gui.transparencyColor,
                                    null
                                 )
                                 ;
                           
                           }
                        
                           DRAW_GRID_LINES:
                           {
                           
                              if (gui.hasGridLines && gui.screenToImagePixelRatio > 1)
                              {
                              
                                 g.setPaint(gui.gridLinesColor);
                                 g.setStroke(new java.awt.BasicStroke(1));
                              
                                 IntStream
                                    .range(rectangle.y, rectangle.y + rectangle.height)
                                    .forEach
                                    (
                                       eachIndex ->
                                       {
                                       
                                          if (eachIndex % gui.screenToImagePixelRatio == 0)
                                          {
                                          
                                             g
                                                .drawLine
                                                (
                                                   rectangle.x,
                                                   eachIndex,
                                                   rectangle.x + rectangle.width,
                                                   eachIndex
                                                )
                                                ;
                                          
                                          }
                                       
                                       }
                                    )
                                    ;
                              
                                 IntStream
                                    .rangeClosed(rectangle.x, rectangle.x + rectangle.width)
                                    .forEach
                                    (
                                       eachIndex ->
                                       {
                                       
                                          if (eachIndex % gui.screenToImagePixelRatio == 0)
                                          {
                                          
                                             g
                                                .drawLine
                                                (
                                                   eachIndex,
                                                   rectangle.y,
                                                   eachIndex,
                                                   rectangle.y + rectangle.height
                                                )
                                                ;
                                          
                                          }
                                       
                                       }
                                    )
                                    ;
                              
                              }
                           
                           }
                        
                        }
                     
                        // gui.drawingAreaScrollPane.repaint();
                        // gui.drawingAreaScrollPane.revalidate();
                     
                     }
                  
                  }
                  ;
            
               box.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
            
               drawingPanel.add(Box.createHorizontalGlue());
               drawingPanel.add(box);
               drawingPanel.add(Box.createHorizontalGlue());
            
               REPAINT_DRAWING_PANEL.run();
            
            }
            ;
      
         RECREATE_DRAWING_AREA_FRESH.run();
      
      }
   
      final JPanel drawingSettingsPanel;
   
      CREATE_DRAWING_SETTINGS_PANEL:
      {
      
         drawingSettingsPanel = new JPanel();
         drawingSettingsPanel.setLayout(new BoxLayout(drawingSettingsPanel, BoxLayout.LINE_AXIS));
      
         final JSpinner screenToImagePixelRatioDropDownMenu;
      
         SCREEN_TO_IMAGE_PIXEL_RATIO_DROP_DOWN_MENU:
         {
         
            screenToImagePixelRatioDropDownMenu =
               new JSpinner
               (
                  new SpinnerNumberModel
                  (
                     this.screenToImagePixelRatio,
                     MIN_SCREEN_TO_IMAGE_PIXEL_RATIO,
                     MAX_SCREEN_TO_IMAGE_PIXEL_RATIO,
                     1
                  )
               )
               ;
         
            screenToImagePixelRatioDropDownMenu
               .addChangeListener
               (
                  event ->
                  {
                  
                     if (!(screenToImagePixelRatioDropDownMenu.getValue() instanceof Integer iii))
                     {
                     
                        throw new IllegalStateException();
                     
                     }
                  
                     this.screenToImagePixelRatio = iii;
                  
                     RECREATE_DRAWING_AREA_FRESH.run();
                  
                  }
               );
         
         }
      
         final JCheckBox hasGridLinesCheckBox;
      
         HAS_GRID_LINES_CHECK_BOX:
         {
         
            hasGridLinesCheckBox = new JCheckBox();
            hasGridLinesCheckBox.setText("Activate Grid Lines");
            hasGridLinesCheckBox.setSelected(true);
            hasGridLinesCheckBox
               .addActionListener
               (
                  event ->
                  {
                  
                     this.hasGridLines = hasGridLinesCheckBox.isSelected();
                  
                     REPAINT_DRAWING_PANEL.run();
                  
                  }
               )
               ;
         
         }
         
         final JButton repaint = new JButton("REPAINT");
         repaint.addActionListener(event -> REPAINT_DRAWING_PANEL.run());
      
         drawingSettingsPanel.add(Box.createHorizontalGlue());
         drawingSettingsPanel.add(screenToImagePixelRatioDropDownMenu);
         drawingSettingsPanel.add(new JLabel("SCREEN pixels = 1 IMAGE pixel"));
         drawingSettingsPanel.add(Box.createHorizontalStrut(10));
         drawingSettingsPanel.add(hasGridLinesCheckBox);
         drawingSettingsPanel.add(Box.createHorizontalStrut(10));
         drawingSettingsPanel.add(repaint);
         drawingSettingsPanel.add(Box.createHorizontalGlue());
      
      }
   
      CREATE_CENTERED_DRAWING_PANEL:
      {
      
         final JPanel centeredDrawingPanel = new JPanel();
         centeredDrawingPanel.setLayout(new BoxLayout(centeredDrawingPanel, BoxLayout.PAGE_AXIS));
         centeredDrawingPanel.add(Box.createVerticalGlue());
         centeredDrawingPanel.add(drawingPanel);
         centeredDrawingPanel.add(Box.createVerticalGlue());
      
         UPDATE_DRAWING_PANEL_BORDER_TEXT =
            () ->
               this.drawingAreaScrollPane
                  .setBorder
                  (
                     BorderFactory
                        .createCompoundBorder
                        (
                           TITLED_BORDER
                              .apply
                              (
                                 "Drawing Area -- "
                                 + gui.image.getHeight()
                                 + " rows and "
                                 + gui.image.getWidth()
                                 + " columns"
                              ),
                           BorderFactory.createLineBorder(Color.BLACK, 1)
                        )
                  )
                  ;
      
         UPDATE_DRAWING_PANEL_BORDER_TEXT.run();
      
         this.drawingAreaScrollPane.setViewportView(centeredDrawingPanel);
      
      }
   
      mainPanel.add(drawingSettingsPanel, BorderLayout.NORTH);
      mainPanel.add(this.drawingAreaScrollPane, BorderLayout.CENTER);
   
      return mainPanel;
   
   }

   private static void print(final String text)
   {
   
      System.out.println(LocalDateTime.now() + " -- " + text);
   
   }

   private Dimension deriveDrawingAreaDimensions()
   {
   
      return
         new Dimension
         (
            this.image.getWidth() * this.screenToImagePixelRatio,
            this.image.getHeight() * this.screenToImagePixelRatio
         )
         ;
   
   }

}

1

There are 1 answers

11
camickr On BEST ANSWER

The more I look at your code the more confused I am. I must admit I don't recognize half of the coding constructs you are using.

However, from what I do understand, it appears to me you don't understand the basics of how custom painting works. You should read the Swing tutorial on Custom Painting for a basic example.

Some tips:

  1. the code for the painting should be self contained in a class.
  2. you pass parameters to the class to control the painting. Parameters such as rows, columns, image, zoom factor.
  3. the paintComponent method will directly reference the above parameters.
  4. you override the getPreferredSize() method to control the size of your component. This method would likely reference your image and the zoom factor. The way you currenlty set the size buy creating a new Box.Filler is unique to say the least.
  5. the code for your ActionListeners should not be recreating the drawing panel. Instead you just invoke a method on the panel to update a property and then invoke revalidate() and repaint().

Because you don't follow the standard coding conventions the code becomes overly complex (to me anyway). I can't think of any good reasons for the code to be structured this way.

I am still confused about your claim that using:

//final Rectangle rectangle  = gui.drawingAreaScrollPane.getViewport().getViewRect();
final Rectangle rectangle  = g.getClipBounds(); 

fixes the problem. As I stated earlier it did not fix the problem for me.

Did you try displaying the Rectangle in both cases to compare the values? Do you understand what the differences mean? I'm not understanding your discussion with MadProgrammer about the "context". In both cases the Rectangle uses the proper x/y location relative to (0, 0) of the components. The difference is that your approach uses a larger area since the width/height values will be the size of the viewport, not the size of the scroll distance.

In any case I downloaded your code again and retested. The strangest thing is that your original code now works (after I made a couple of coding changes since your syntax doesn't work for me with JDK11).

I also simplified the drawImage statement to just use:

g.drawImage(gui.image, 0, 0, image.getWidth() * gui.screenToImagePixelRatio, image.getHeight() * gui.screenToImagePixelRatio, null);

and it still works for me.

So for what its worth here is the version of the code I'm testing on Windows 11, JDK11 that works:

import javax.imageio.*;
import javax.imageio.event.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.colorchooser.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.File;
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.*;

public class GUI
{

   private static final Function<String, Border> TITLED_BORDER =
      title ->
         BorderFactory
            .createTitledBorder
            (
               null,
               title,
               TitledBorder.CENTER,
               TitledBorder.TOP
            )
            ;

   private static final Color CLEAR = new Color(0, 0, 0, 0);
   private static final Point OFF_SCREEN = new Point(-1, -1);

   public static final int ARBITRARY_VIEW_BUFFER = 200;
   private static final int MIN_PEN_SIZE = 1;
   private static final int MAX_PEN_SIZE = 10;
   private static final int MIN_SCREEN_TO_IMAGE_PIXEL_RATIO = 1;
   private static final int MAX_SCREEN_TO_IMAGE_PIXEL_RATIO = 30;
   private static final int DEFAULT_IMAGE_PIXEL_ROWS = 26;
   private static final int DEFAULT_IMAGE_PIXEL_COLUMNS = 24;

   private final JFrame frame;
   private final JScrollPane drawingAreaScrollPane = new JScrollPane();

   private BufferedImage image;
   private Color transparencyColor = Color.WHITE;
   private Color cursorColor = Color.BLACK;
   private Color gridLinesColor = Color.GRAY;
   private boolean hasGridLines = true;
   private int penSize = 1;
   private int screenToImagePixelRatio = 10;

   public static void main(final String[] args)
   {
   
      final BufferedImage image = new BufferedImage(20, 20, BufferedImage.TYPE_INT_ARGB);
   
      IntStream
         .range(0, 20)
         .forEach(i -> image.setRGB(i, i, Color.RED.getRGB()))
         ;

      new GUI(image);
   
   }

   public GUI()
   {
   
      this(DEFAULT_IMAGE_PIXEL_ROWS, DEFAULT_IMAGE_PIXEL_COLUMNS);
   
   }

   public GUI(final int numImagePixelRows, final int numImagePixelColumns)
   {
   
      this(new BufferedImage(numImagePixelColumns, numImagePixelRows, BufferedImage.TYPE_INT_ARGB));
   
   }

   public GUI(final BufferedImage image)
   {
   
      Objects.requireNonNull(image);
   
      try
      {
      
         UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());

      }
      
      catch (Exception e)
      {
      
         throw new RuntimeException(e);
      
      }
   
      INITALIZING_METADATA_INSTANCE_FIELDS:
      {
      
         this.image = image;
      
      }

      this.frame = new JFrame();
   
      this.frame.setTitle("Paint");
      this.frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
   
      this.frame.add(this.createMainPanel());
   
      this.frame.pack();
      this.frame.setLocationByPlatform(true);
      this.frame.setVisible(true);
   
   }

   private JPanel createMainPanel()
   {
   
      final JPanel mainPanel = new JPanel(new BorderLayout());
   
      mainPanel.add(this.createCenterPanel(),   BorderLayout.CENTER);
   
      return mainPanel;
   
   }

   private JPanel createCenterPanel()
   {
   
      final GUI gui = this; //useful when trying to differentiate between different this'.
   
      final JPanel mainPanel;
   
      CREATE_MAIN_PANEL:
      {

         mainPanel = new JPanel();
         mainPanel.setLayout(new BorderLayout());
      
      }
   
      final JPanel drawingPanel;
   
      final Runnable UPDATE_DRAWING_PANEL_BORDER_TEXT;
      final Runnable RECREATE_DRAWING_AREA_FRESH;
      final Runnable REPAINT_DRAWING_PANEL;
   
      final BiFunction<Point, Integer, Point> originalToZoomed =
         (original, ratio) ->
            new Point(original.x * ratio, original.y * ratio)
            ;
   
      final BiFunction<Point, Integer, Point> zoomedToOriginal =
         (zoomed, ratio) ->
            new Point
            (
               (zoomed.x - (zoomed.x % ratio)) / ratio,
               (zoomed.y - (zoomed.y % ratio)) / ratio
            )
            ;

      CREATE_DRAWING_PANEL:
      {

         SET_UP_DRAWING_PANEL:
         {

            drawingPanel = new JPanel();

            drawingPanel.setLayout(new BoxLayout(drawingPanel, BoxLayout.PAGE_AXIS));

            drawingPanel.add(Box.createRigidArea(new Dimension(123, 456)));

         }

         REPAINT_DRAWING_PANEL =
            () ->
            {

               drawingPanel.repaint();
               drawingPanel.revalidate();

            }
            ;

         RECREATE_DRAWING_AREA_FRESH =
            () ->
            {

               final var thingToFocus = drawingPanel;

               drawingPanel.removeAll();

               final Dimension drawingArea = gui.deriveDrawingAreaDimensions();

               final IntUnaryOperator quantize =
                  num ->
                     num - (num % gui.screenToImagePixelRatio)
                     ;

               final Box.Filler box =
                  new Box.Filler(drawingArea, drawingArea, drawingArea)
                  {

                     @Override
                     protected void paintComponent(final Graphics dontUse)
                     {

                        super.paintComponent(dontUse);

/*
                        if (!(dontUse instanceof Graphics2D g))
                        {

                           throw new RuntimeException("Unknown graphics type = " + dontUse);

                        }
*/
                        Graphics2D g = (Graphics2D)dontUse;

                        DRAW_DRAWN_PIXELS:
                        {

                           final Rectangle rectangle  = gui.drawingAreaScrollPane.getViewport().getViewRect();
//                           final Rectangle rectangle  = g.getClipBounds();

                           //We are only drawing a subsection because we may be working with GIGANTIC images.
                           //If we attempt to draw the whole image, performance will drop like a rock.
                           CALCULATE_SUBSECTION_TO_DRAW:
                           {

                              final int x = rectangle.x;
                              final int y = rectangle.y;
                              final int width = Math.min(rectangle.width, drawingArea.width);
                              final int height = Math.min(rectangle.height, drawingArea.height);

                              g.setBackground(gui.transparencyColor);
                              g.clearRect(rectangle.x, rectangle.y, width, height);

                           }

                           DRAW_SUBSECTION_OF_IMAGE:
                           {

                              final int originalImageX;
                              final int zoomedInImageX;
                              final int originalImageY;
                              final int zoomedInImageY;

                              CALCULATE_ORIGINAL_POSITION:
                              {

                                 final int quantizedX = quantize.applyAsInt(rectangle.x);
                                 final int quantizedY = quantize.applyAsInt(rectangle.y);

                                 zoomedInImageX = Math.max(quantizedX, 0);
                                 originalImageX = zoomedInImageX / gui.screenToImagePixelRatio;

                                 zoomedInImageY = Math.max(quantizedY, 0);
                                 originalImageY = zoomedInImageY / gui.screenToImagePixelRatio;

                              }

                              final int zoomedInImageWidth;
                              final int originalImageWidth;
                              final int zoomedInImageHeight;
                              final int originalImageHeight;

                              CALCULATE_ORIGINAL_DIMENSION:
                              {

                                 final int minWidth = Math.min(rectangle.width, drawingArea.width);
                                 final int quantizedMinWidth = quantize.applyAsInt(minWidth);
                                 final int potentialWidth = quantizedMinWidth + gui.screenToImagePixelRatio;
                                 zoomedInImageWidth = Math.min(drawingArea.width, potentialWidth);
                                 originalImageWidth = zoomedInImageWidth / gui.screenToImagePixelRatio;

                                 final int minHeight = Math.min(rectangle.height, drawingArea.height);
                                 final int quantizedMinHeight = quantize.applyAsInt(minHeight);
                                 final int potentialHeight = quantizedMinHeight + gui.screenToImagePixelRatio;
                                 zoomedInImageHeight = Math.min(drawingArea.height, potentialHeight);
                                 originalImageHeight = zoomedInImageHeight / gui.screenToImagePixelRatio;

                              }

                              g.setPaint(gui.cursorColor);

//                              System.out.println(zoomedInImageX + " -- " + zoomedInImageY + " -- " + zoomedInImageWidth + " -- " + zoomedInImageHeight + " ---- " + originalImageX + " -- " + originalImageY + " -- " + originalImageWidth + " -- " + originalImageHeight + " ----- " + rectangle + " - " + drawingArea);

                                g.drawImage(gui.image, 0, 0, image.getWidth() * gui.screenToImagePixelRatio, image.getHeight() * gui.screenToImagePixelRatio, null);
/*
                              g
                                 .drawImage
                                 (
                                    gui.image,
                                    zoomedInImageX,
                                    zoomedInImageY,
                                    zoomedInImageX + zoomedInImageWidth,
                                    zoomedInImageY + zoomedInImageHeight,
                                    originalImageX,
                                    originalImageY,
                                    originalImageX + originalImageWidth,
                                    originalImageY + originalImageHeight,
                                    //gui.transparencyColor,
                                    null
                                 )
                                 ;
*/
                           }

                           DRAW_GRID_LINES:
                           {

                              if (gui.hasGridLines && gui.screenToImagePixelRatio > 1)
                              {

                                 g.setPaint(gui.gridLinesColor);
                                 g.setStroke(new java.awt.BasicStroke(1));

                                 IntStream
                                    .range(rectangle.y, rectangle.y + rectangle.height)
                                    .forEach
                                    (
                                       eachIndex ->
                                       {

                                          if (eachIndex % gui.screenToImagePixelRatio == 0)
                                          {

                                             g
                                                .drawLine
                                                (
                                                   rectangle.x,
                                                   eachIndex,
                                                   rectangle.x + rectangle.width,
                                                   eachIndex
                                                )
                                                ;

                                          }

                                       }
                                    )
                                    ;

                                 IntStream
                                    .rangeClosed(rectangle.x, rectangle.x + rectangle.width)
                                    .forEach
                                    (
                                       eachIndex ->
                                       {

                                          if (eachIndex % gui.screenToImagePixelRatio == 0)
                                          {

                                             g
                                                .drawLine
                                                (
                                                   eachIndex,
                                                   rectangle.y,
                                                   eachIndex,
                                                   rectangle.y + rectangle.height
                                                )
                                                ;

                                          }

                                       }
                                    )
                                    ;

                              }

                           }

                        }

                        // gui.drawingAreaScrollPane.repaint();
                        // gui.drawingAreaScrollPane.revalidate();
                     
                     }
                  
                  }
                  ;
            
               box.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
            
               drawingPanel.add(Box.createHorizontalGlue());
               drawingPanel.add(box);
               drawingPanel.add(Box.createHorizontalGlue());
            
               REPAINT_DRAWING_PANEL.run();
            
            }
            ;
      
         RECREATE_DRAWING_AREA_FRESH.run();
      
      }
   
      final JPanel drawingSettingsPanel;
   
      CREATE_DRAWING_SETTINGS_PANEL:
      {
      
         drawingSettingsPanel = new JPanel();
         drawingSettingsPanel.setLayout(new BoxLayout(drawingSettingsPanel, BoxLayout.LINE_AXIS));
      
         final JSpinner screenToImagePixelRatioDropDownMenu;
      
         SCREEN_TO_IMAGE_PIXEL_RATIO_DROP_DOWN_MENU:
         {
         
            screenToImagePixelRatioDropDownMenu =
               new JSpinner
               (
                  new SpinnerNumberModel
                  (
                     this.screenToImagePixelRatio,
                     MIN_SCREEN_TO_IMAGE_PIXEL_RATIO,
                     MAX_SCREEN_TO_IMAGE_PIXEL_RATIO,
                     1
                  )
               )
               ;
         
            screenToImagePixelRatioDropDownMenu
               .addChangeListener
               (
                  event ->
                  {
                  
/*
                     if (!(screenToImagePixelRatioDropDownMenu.getValue() instanceof Integer iii))
                     {

                        throw new IllegalStateException();

                     }
*/
                     int iii = ((Integer)screenToImagePixelRatioDropDownMenu.getValue()).intValue();
                     this.screenToImagePixelRatio = iii;

                     RECREATE_DRAWING_AREA_FRESH.run();

                  }
               );

         }

         final JCheckBox hasGridLinesCheckBox;
      
         HAS_GRID_LINES_CHECK_BOX:
         {
         
            hasGridLinesCheckBox = new JCheckBox();
            hasGridLinesCheckBox.setText("Activate Grid Lines");
            hasGridLinesCheckBox.setSelected(true);
            hasGridLinesCheckBox
               .addActionListener
               (
                  event ->
                  {
                  
                     this.hasGridLines = hasGridLinesCheckBox.isSelected();
                  
                     REPAINT_DRAWING_PANEL.run();
                  
                  }
               )
               ;
         
         }
         
         final JButton repaint = new JButton("REPAINT");
         repaint.addActionListener(event -> REPAINT_DRAWING_PANEL.run());
      
         drawingSettingsPanel.add(Box.createHorizontalGlue());
         drawingSettingsPanel.add(screenToImagePixelRatioDropDownMenu);
         drawingSettingsPanel.add(new JLabel("SCREEN pixels = 1 IMAGE pixel"));
         drawingSettingsPanel.add(Box.createHorizontalStrut(10));
         drawingSettingsPanel.add(hasGridLinesCheckBox);
         drawingSettingsPanel.add(Box.createHorizontalStrut(10));
         drawingSettingsPanel.add(repaint);
         drawingSettingsPanel.add(Box.createHorizontalGlue());
      
      }
   
      CREATE_CENTERED_DRAWING_PANEL:
      {
      
         final JPanel centeredDrawingPanel = new JPanel();
         centeredDrawingPanel.setLayout(new BoxLayout(centeredDrawingPanel, BoxLayout.PAGE_AXIS));
         centeredDrawingPanel.add(Box.createVerticalGlue());
         centeredDrawingPanel.add(drawingPanel);
         centeredDrawingPanel.add(Box.createVerticalGlue());
      
         UPDATE_DRAWING_PANEL_BORDER_TEXT =
            () ->
               this.drawingAreaScrollPane
                  .setBorder
                  (
                     BorderFactory
                        .createCompoundBorder
                        (
                           TITLED_BORDER
                              .apply
                              (
                                 "Drawing Area -- "
                                 + gui.image.getHeight()
                                 + " rows and "
                                 + gui.image.getWidth()
                                 + " columns"
                              ),
                           BorderFactory.createLineBorder(Color.BLACK, 1)
                        )
                  )
                  ;
      
         UPDATE_DRAWING_PANEL_BORDER_TEXT.run();
      
         this.drawingAreaScrollPane.setViewportView(centeredDrawingPanel);
      
      }
   
      mainPanel.add(drawingSettingsPanel, BorderLayout.NORTH);
      mainPanel.add(this.drawingAreaScrollPane, BorderLayout.CENTER);
   
      return mainPanel;
   
   }

   private static void print(final String text)
   {
   
      System.out.println(LocalDateTime.now() + " -- " + text);
   
   }

   private Dimension deriveDrawingAreaDimensions()
   {
   
      return
         new Dimension
         (
            this.image.getWidth() * this.screenToImagePixelRatio,
            this.image.getHeight() * this.screenToImagePixelRatio
         )
         ;
   
   }

}

Note: just because it works doesn't mean I think you should use it. I believe the code should be restructured to better follow Swing custom painting conventions.