All Downloads are FREE. Search and download functionalities are using the official Maven repository.

boofcv.gui.image.ImageZoomPanel Maven / Gradle / Ivy

/*
 * Copyright (c) 2021, Peter Abeles. All Rights Reserved.
 *
 * This file is part of BoofCV (http://boofcv.org).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package boofcv.gui.image;

import boofcv.io.image.ConvertBufferedImage;
import georegression.struct.point.Point2D_F64;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.Objects;

/**
 * Simple JPanel for displaying buffered images allows images to be zoomed in and out of
 *
 * @author Peter Abeles
 */
@SuppressWarnings({"NullAway.Init"})
public class ImageZoomPanel extends JScrollPane {
	// the image being displayed
	protected @Nullable BufferedImage img;
	protected ImagePanel panel = new ImagePanel();

	public boolean autoScaleCenterOnSetImage = true;
	private boolean hasImageChanged = false;

	protected double scale = 1;
	protected double transX = 0, transY = 0;

	boolean checkEventDispatch = true;

	protected ImageZoomListener listener;

	public ImageZoomPanel( final @Nullable BufferedImage img ) {
		this.img = img;
		setScale(1);
	}

	public ImageZoomPanel() {
		getViewport().setView(panel);
	}

	/**
	 * Makes it so that the image is no longer saved when clicked.
	 */
	public void disableSaveOnClick() {
		panel.removeMouseListener(panel.mouseListener);
		panel.mouseListener = null;
	}

	public void autoScaleAndAlign() {
		this.autoScaleAndAlign(Objects.requireNonNull(img));
	}

	/**
	 * Automatically recomputes the scale to fit inside the window and centers it in the view
	 */
	private void autoScaleAndAlign( final BufferedImage img ) {
		double ratioW = (double)getWidth()/(double)img.getWidth();
		double ratioH = (double)getHeight()/(double)img.getHeight();

		double scale = Math.min(ratioW, ratioH);
		if (scale >= 1)
			scale = 1;

		boolean scaleChange = this.scale != scale;
		this.scale = scale;
		if (scaleChange) {
			if (listener != null) {
				listener.handleScaleChange(this.scale);
			}
			updateSize(img.getWidth(), img.getHeight());
		}
		// Scroll it to the top left (this is the align part)
		getHorizontalScrollBar().setValue(0);
		getVerticalScrollBar().setValue(0);
	}

	public synchronized void setScale( double scale ) {
		// do nothing if the scale has not changed
		if (this.scale == scale)
			return;

		Rectangle r = panel.getVisibleRect();
		double centerX = (r.x + r.width/2.0)/this.scale;
		double centerY = (r.y + r.height/2.0)/this.scale;

		this.scale = scale;
		if (img != null) {
			int w = (int)Math.ceil(img.getWidth()*scale);
			int h = (int)Math.ceil(img.getHeight()*scale);
			panel.setPreferredSize(new Dimension(w, h));
		}
		getViewport().setView(panel);

		centerView(centerX, centerY);

		{
			ImageZoomListener listener = this.listener;
			if (listener != null) {
				listener.handleScaleChange(this.scale);
			}
		}
	}

	public synchronized void setScaleAndCenter( double scale, double cx, double cy ) {
		boolean scaleChanged = false;
		if (scale != this.scale) {
			scaleChanged = true;
			this.scale = scale;
		}
		if (img != null) {
			int w = (int)Math.ceil(img.getWidth()*scale);
			int h = (int)Math.ceil(img.getHeight()*scale);
			panel.setPreferredSize(new Dimension(w, h));
		}
		getViewport().setView(panel);
		centerView(cx, cy);

		if (scaleChanged) {
			ImageZoomListener listener = this.listener;
			if (listener != null) {
				listener.handleScaleChange(this.scale);
			}
		}
	}

	public synchronized void centerView( double cx, double cy ) {
		Rectangle r = panel.getVisibleRect();
		int x = (int)(cx*scale - r.width/2);
		int y = (int)(cy*scale - r.height/2);

		getHorizontalScrollBar().setValue(x);
		getVerticalScrollBar().setValue(y);
	}

	/**
	 * Change the image being displayed.
	 *
	 * @param image The new image which will be displayed.
	 */
	public synchronized void setImage( BufferedImage image ) {
		// assume the image was initially set before the GUI was invoked
		if (checkEventDispatch && this.img != null) {
			if (!SwingUtilities.isEventDispatchThread())
				throw new RuntimeException("Changed image when not in GUI thread?");
		}

		hasImageChanged = this.img != image;
		int beforeWidth = -1, beforeHeight = -1;
		if (this.img != null) {
			beforeWidth = this.img.getWidth();
			beforeHeight = this.img.getHeight();
		}
		setBufferedImageNoChange(image);
		// only update size if the image size has changed
		if (image != null && hasImageChanged && beforeWidth != image.getWidth() && beforeHeight != image.getHeight()) {
			updateSize(image.getWidth(), image.getHeight());
		}
	}

	public void updateSize( int width, int height ) {
		Dimension prev = getPreferredSize();

		int w = (int)Math.ceil(width*scale);
		int h = (int)Math.ceil(height*scale);

		if (prev.getWidth() != w || prev.getHeight() != h) {
			panel.setPreferredSize(new Dimension(w, h));
			getViewport().setView(panel);
		}
	}

	public synchronized void setBufferedImageNoChange( @Nullable BufferedImage image ) {
		// assume the image was initially set before the GUI was invoked
		if (checkEventDispatch && this.img != null) {
			if (!SwingUtilities.isEventDispatchThread())
				throw new RuntimeException("Changed image when not in GUI thread?");
		}
		this.img = image;
	}

	public @Nullable BufferedImage getImage() {
		return img;
	}

	public Point2D_F64 pixelToPoint( int x, int y ) {
		Point2D_F64 ret = new Point2D_F64();
		ret.x = x/scale;
		ret.y = y/scale;

		return ret;
	}

	/**
	 * Paint inside the image panel. Useful for overlays
	 */
	protected void paintInPanel( AffineTransform tran, Graphics2D g2 ) {
		// intentionally empty
	}

	/** Called very last when you don't want to draw in image coordinates */
	protected void paintOverPanel( Graphics2D g2 ) {}

	public class ImagePanel extends JPanel {
		@Nullable SaveImageOnClick mouseListener = new SaveImageOnClick(ImageZoomPanel.this.getViewport());

		BufferedImage buffer = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);

		public ImagePanel() {
			addMouseListener(mouseListener);
		}

		@Override
		public void paintComponent( Graphics g ) {
			super.paintComponent(g);

			if (img == null) {
				paintOverPanel((Graphics2D)g);
				return;
			}

			if (hasImageChanged) {
				hasImageChanged = false;
				if (autoScaleCenterOnSetImage) {
					autoScaleAndAlign();
				}
			}
			Graphics2D panelGraphics = (Graphics2D)g;

			// render to a buffer first to improve performance. This is particularly evident when rendering
			// lines for some reason

			int w = ImageZoomPanel.this.getWidth(), h = ImageZoomPanel.this.getHeight();
			buffer = ConvertBufferedImage.checkDeclare(w, h, buffer, buffer.getType());
			Graphics2D bufferGraphics = this.buffer.createGraphics();
			bufferGraphics.setColor(getBackground());
			bufferGraphics.fillRect(0, 0, w, h);

			ImageZoomPanel.this.transX = -ImageZoomPanel.this.getHorizontalScrollBar().getValue();
			ImageZoomPanel.this.transY = -ImageZoomPanel.this.getVerticalScrollBar().getValue();

			AffineTransform tran = new AffineTransform(scale, 0, 0, scale, transX, transY);
			synchronized (ImageZoomPanel.this) {
				bufferGraphics.drawImage(img, tran, null);
			}

			AffineTransform orig = bufferGraphics.getTransform();
			// make the default behavior be a translate for backwards compatibility. Before it was rendered in a
			// buffered image it was rendered into the full scale panel
			tran = new AffineTransform(1, 0, 0, 1, transX, transY);
			bufferGraphics.setTransform(tran);
			paintInPanel(tran, bufferGraphics);
			bufferGraphics.setTransform(orig);

			panelGraphics.drawImage(this.buffer, (int)-transX, (int)-transY, null);

			paintOverPanel(panelGraphics);
		}
	}

	public void setScrollbarsVisible( boolean visible ) {
		if (visible) {
			setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
			setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
		} else {
			setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
			setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
		}
		repaint();
	}

	public double getScale() {
		return scale;
	}

	public ImagePanel getImagePanel() {
		return panel;
	}

	public ImageZoomListener getListener() {
		return listener;
	}

	public void setListener( ImageZoomListener listener ) {
		this.listener = listener;
	}

	public interface ImageZoomListener {
		void handleScaleChange( double scale );
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy