001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer.geoimage; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Color; 007import java.awt.Dimension; 008import java.awt.FontMetrics; 009import java.awt.Graphics; 010import java.awt.Graphics2D; 011import java.awt.Image; 012import java.awt.MediaTracker; 013import java.awt.Point; 014import java.awt.Rectangle; 015import java.awt.RenderingHints; 016import java.awt.Toolkit; 017import java.awt.event.MouseEvent; 018import java.awt.event.MouseListener; 019import java.awt.event.MouseMotionListener; 020import java.awt.event.MouseWheelEvent; 021import java.awt.event.MouseWheelListener; 022import java.awt.geom.AffineTransform; 023import java.awt.geom.Rectangle2D; 024import java.awt.image.BufferedImage; 025import java.awt.image.ImageObserver; 026import java.io.File; 027 028import javax.swing.JComponent; 029import javax.swing.SwingUtilities; 030 031import org.openstreetmap.josm.data.preferences.BooleanProperty; 032import org.openstreetmap.josm.data.preferences.DoubleProperty; 033import org.openstreetmap.josm.spi.preferences.Config; 034import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 035import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 036import org.openstreetmap.josm.tools.Destroyable; 037import org.openstreetmap.josm.tools.ExifReader; 038import org.openstreetmap.josm.tools.ImageProvider; 039import org.openstreetmap.josm.tools.Logging; 040 041/** 042 * GUI component to display an image (photograph). 043 * 044 * Offers basic mouse interaction (zoom, drag) and on-screen text. 045 */ 046public class ImageDisplay extends JComponent implements Destroyable, PreferenceChangedListener { 047 048 /** The file that is currently displayed */ 049 private ImageEntry entry; 050 051 /** The image currently displayed */ 052 private transient Image image; 053 054 /** The image currently displayed */ 055 private boolean errorLoading; 056 057 /** The rectangle (in image coordinates) of the image that is visible. This rectangle is calculated 058 * each time the zoom is modified */ 059 private VisRect visibleRect; 060 061 /** When a selection is done, the rectangle of the selection (in image coordinates) */ 062 private VisRect selectedRect; 063 064 /** The tracker to load the images */ 065 private final MediaTracker tracker = new MediaTracker(this); 066 067 private final ImgDisplayMouseListener imgMouseListener = new ImgDisplayMouseListener(); 068 069 private String emptyText; 070 private String osdText; 071 072 private static final BooleanProperty AGPIFO_STYLE = 073 new BooleanProperty("geoimage.agpifo-style-drag-and-zoom", false); 074 private static int dragButton; 075 private static int zoomButton; 076 077 /** Alternative to mouse wheel zoom; esp. handy if no mouse wheel is present **/ 078 private static final BooleanProperty ZOOM_ON_CLICK = 079 new BooleanProperty("geoimage.use-mouse-clicks-to-zoom", true); 080 081 /** Zoom factor when click or wheel zooming **/ 082 private static final DoubleProperty ZOOM_STEP = 083 new DoubleProperty("geoimage.zoom-step-factor", 3 / 2.0); 084 085 /** Maximum zoom allowed **/ 086 private static final DoubleProperty MAX_ZOOM = 087 new DoubleProperty("geoimage.maximum-zoom-scale", 2.0); 088 089 /** Use bilinear filtering **/ 090 private static final BooleanProperty BILIN_DOWNSAMP = 091 new BooleanProperty("geoimage.bilinear-downsampling-progressive", true); 092 private static final BooleanProperty BILIN_UPSAMP = 093 new BooleanProperty("geoimage.bilinear-upsampling", false); 094 private static double bilinUpper; 095 private static double bilinLower; 096 097 @Override 098 public void preferenceChanged(PreferenceChangeEvent e) { 099 if (e == null || 100 e.getKey().equals(AGPIFO_STYLE.getKey())) { 101 dragButton = AGPIFO_STYLE.get() ? 1 : 3; 102 zoomButton = dragButton == 1 ? 3 : 1; 103 } 104 if (e == null || 105 e.getKey().equals(MAX_ZOOM.getKey()) || 106 e.getKey().equals(BILIN_DOWNSAMP.getKey()) || 107 e.getKey().equals(BILIN_UPSAMP.getKey())) { 108 bilinUpper = (BILIN_UPSAMP.get() ? 2*MAX_ZOOM.get() : (BILIN_DOWNSAMP.get() ? 0.5 : 0)); 109 bilinLower = (BILIN_DOWNSAMP.get() ? 0 : 1); 110 } 111 } 112 113 /** 114 * Manage the visible rectangle of an image with full bounds stored in init. 115 * @since 13127 116 */ 117 public static class VisRect extends Rectangle { 118 private final Rectangle init; 119 120 /** set when this {@code VisRect} is updated by a mouse drag operation and 121 * unset on mouse release **/ 122 public boolean isDragUpdate; 123 124 /** 125 * Constructs a new {@code VisRect}. 126 * @param x the specified X coordinate 127 * @param y the specified Y coordinate 128 * @param width the width of the rectangle 129 * @param height the height of the rectangle 130 */ 131 public VisRect(int x, int y, int width, int height) { 132 super(x, y, width, height); 133 init = new Rectangle(this); 134 } 135 136 /** 137 * Constructs a new {@code VisRect}. 138 * @param x the specified X coordinate 139 * @param y the specified Y coordinate 140 * @param width the width of the rectangle 141 * @param height the height of the rectangle 142 * @param peer share full bounds with this peer {@code VisRect} 143 */ 144 public VisRect(int x, int y, int width, int height, VisRect peer) { 145 super(x, y, width, height); 146 init = peer.init; 147 } 148 149 /** 150 * Constructs a new {@code VisRect} from another one. 151 * @param v rectangle to copy 152 */ 153 public VisRect(VisRect v) { 154 super(v); 155 init = v.init; 156 } 157 158 /** 159 * Constructs a new empty {@code VisRect}. 160 */ 161 public VisRect() { 162 this(0, 0, 0, 0); 163 } 164 165 public boolean isFullView() { 166 return init.equals(this); 167 } 168 169 public boolean isFullView1D() { 170 return (init.x == x && init.width == width) 171 || (init.y == y && init.height == height); 172 } 173 174 public void reset() { 175 setBounds(init); 176 } 177 178 public void checkRectPos() { 179 if (x < 0) { 180 x = 0; 181 } 182 if (y < 0) { 183 y = 0; 184 } 185 if (x + width > init.width) { 186 x = init.width - width; 187 } 188 if (y + height > init.height) { 189 y = init.height - height; 190 } 191 } 192 193 public void checkRectSize() { 194 if (width > init.width) { 195 width = init.width; 196 } 197 if (height > init.height) { 198 height = init.height; 199 } 200 } 201 202 public void checkPointInside(Point p) { 203 if (p.x < x) { 204 p.x = x; 205 } 206 if (p.x > x + width) { 207 p.x = x + width; 208 } 209 if (p.y < y) { 210 p.y = y; 211 } 212 if (p.y > y + height) { 213 p.y = y + height; 214 } 215 } 216 } 217 218 /** The thread that reads the images. */ 219 private class LoadImageRunnable implements Runnable, ImageObserver { 220 221 private final ImageEntry entry; 222 private final File file; 223 224 LoadImageRunnable(ImageEntry entry) { 225 this.entry = entry; 226 this.file = entry.getFile(); 227 } 228 229 @Override 230 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { 231 if (((infoflags & ImageObserver.WIDTH) == ImageObserver.WIDTH) && 232 ((infoflags & ImageObserver.HEIGHT) == ImageObserver.HEIGHT)) { 233 synchronized (entry) { 234 entry.setWidth(width); 235 entry.setHeight(height); 236 entry.notifyAll(); 237 return false; 238 } 239 } 240 return true; 241 } 242 243 private boolean updateImageEntry(Image img) { 244 if (!(entry.getWidth() > 0 && entry.getHeight() > 0)) { 245 synchronized (entry) { 246 img.getWidth(this); 247 img.getHeight(this); 248 249 long now = System.currentTimeMillis(); 250 while (!(entry.getWidth() > 0 && entry.getHeight() > 0)) { 251 try { 252 entry.wait(1000); 253 if (this.entry != ImageDisplay.this.entry) 254 return false; 255 if (System.currentTimeMillis() - now > 10000) 256 synchronized (ImageDisplay.this) { 257 errorLoading = true; 258 ImageDisplay.this.repaint(); 259 return false; 260 } 261 } catch (InterruptedException e) { 262 Logging.trace(e); 263 Logging.warn("InterruptedException in {0} while getting properties of image {1}", 264 getClass().getSimpleName(), file.getPath()); 265 Thread.currentThread().interrupt(); 266 } 267 } 268 } 269 } 270 return true; 271 } 272 273 private boolean mayFitMemory(long amountWanted) { 274 return amountWanted < ( 275 Runtime.getRuntime().maxMemory() - 276 Runtime.getRuntime().totalMemory() + 277 Runtime.getRuntime().freeMemory()); 278 } 279 280 @Override 281 public void run() { 282 Image img = Toolkit.getDefaultToolkit().createImage(file.getPath()); 283 if (!updateImageEntry(img)) 284 return; 285 286 int width = entry.getWidth(); 287 int height = entry.getHeight(); 288 289 if (mayFitMemory(((long) width)*height*4*2)) { 290 Logging.info("Loading {0} using default toolkit", file.getPath()); 291 tracker.addImage(img, 1); 292 293 // Wait for the end of loading 294 while (!tracker.checkID(1, true)) { 295 if (this.entry != ImageDisplay.this.entry) { 296 // The file has changed 297 tracker.removeImage(img); 298 return; 299 } 300 try { 301 Thread.sleep(5); 302 } catch (InterruptedException e) { 303 Logging.trace(e); 304 Logging.warn("InterruptedException in {0} while loading image {1}", 305 getClass().getSimpleName(), file.getPath()); 306 Thread.currentThread().interrupt(); 307 } 308 } 309 if (tracker.isErrorID(1)) { 310 // the tracker catches OutOfMemory conditions 311 tracker.removeImage(img); 312 img = null; 313 } else { 314 tracker.removeImage(img); 315 } 316 } else { 317 img = null; 318 } 319 320 synchronized (ImageDisplay.this) { 321 if (this.entry != ImageDisplay.this.entry) { 322 // The file has changed 323 return; 324 } 325 326 if (img != null) { 327 boolean switchedDim = false; 328 if (ExifReader.orientationNeedsCorrection(entry.getExifOrientation())) { 329 if (ExifReader.orientationSwitchesDimensions(entry.getExifOrientation())) { 330 width = img.getHeight(null); 331 height = img.getWidth(null); 332 switchedDim = true; 333 } 334 final BufferedImage rot = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 335 final AffineTransform xform = ExifReader.getRestoreOrientationTransform( 336 entry.getExifOrientation(), 337 img.getWidth(null), 338 img.getHeight(null)); 339 final Graphics2D g = rot.createGraphics(); 340 g.drawImage(img, xform, null); 341 g.dispose(); 342 img = rot; 343 } 344 345 ImageDisplay.this.image = img; 346 visibleRect = new VisRect(0, 0, width, height); 347 348 Logging.info("Loaded {0} with dimensions {1}x{2} memoryTaken={3}m exifOrientationSwitchedDimension={4}", 349 file.getPath(), width, height, width*height*4/1024/1024, switchedDim); 350 } 351 352 selectedRect = null; 353 errorLoading = (img == null); 354 } 355 ImageDisplay.this.repaint(); 356 } 357 } 358 359 private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener { 360 361 private MouseEvent lastMouseEvent; 362 private Point mousePointInImg; 363 364 private boolean mouseIsDragging(MouseEvent e) { 365 return (dragButton == 1 && SwingUtilities.isLeftMouseButton(e)) || 366 (dragButton == 2 && SwingUtilities.isMiddleMouseButton(e)) || 367 (dragButton == 3 && SwingUtilities.isRightMouseButton(e)); 368 } 369 370 private boolean mouseIsZoomSelecting(MouseEvent e) { 371 return (zoomButton == 1 && SwingUtilities.isLeftMouseButton(e)) || 372 (zoomButton == 2 && SwingUtilities.isMiddleMouseButton(e)) || 373 (zoomButton == 3 && SwingUtilities.isRightMouseButton(e)); 374 } 375 376 private boolean isAtMaxZoom(Rectangle visibleRect) { 377 return (visibleRect.width == (int) (getSize().width / MAX_ZOOM.get()) || 378 visibleRect.height == (int) (getSize().height / MAX_ZOOM.get())); 379 } 380 381 private void mouseWheelMovedImpl(int x, int y, int rotation, boolean refreshMousePointInImg) { 382 ImageEntry entry; 383 Image image; 384 VisRect visibleRect; 385 386 synchronized (ImageDisplay.this) { 387 entry = ImageDisplay.this.entry; 388 image = ImageDisplay.this.image; 389 visibleRect = ImageDisplay.this.visibleRect; 390 } 391 392 selectedRect = null; 393 394 if (image == null) 395 return; 396 397 // Calculate the mouse cursor position in image coordinates to center the zoom. 398 if (refreshMousePointInImg) 399 mousePointInImg = comp2imgCoord(visibleRect, x, y, getSize()); 400 401 // Apply the zoom to the visible rectangle in image coordinates 402 if (rotation > 0) { 403 visibleRect.width = (int) (visibleRect.width * ZOOM_STEP.get()); 404 visibleRect.height = (int) (visibleRect.height * ZOOM_STEP.get()); 405 } else { 406 visibleRect.width = (int) (visibleRect.width / ZOOM_STEP.get()); 407 visibleRect.height = (int) (visibleRect.height / ZOOM_STEP.get()); 408 } 409 410 // Check that the zoom doesn't exceed MAX_ZOOM:1 411 if (visibleRect.width < getSize().width / MAX_ZOOM.get()) { 412 visibleRect.width = (int) (getSize().width / MAX_ZOOM.get()); 413 } 414 if (visibleRect.height < getSize().height / MAX_ZOOM.get()) { 415 visibleRect.height = (int) (getSize().height / MAX_ZOOM.get()); 416 } 417 418 // Set the same ratio for the visible rectangle and the display area 419 int hFact = visibleRect.height * getSize().width; 420 int wFact = visibleRect.width * getSize().height; 421 if (hFact > wFact) { 422 visibleRect.width = hFact / getSize().height; 423 } else { 424 visibleRect.height = wFact / getSize().width; 425 } 426 427 // The size of the visible rectangle is limited by the image size. 428 visibleRect.checkRectSize(); 429 430 // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image. 431 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, getSize()); 432 visibleRect.x = mousePointInImg.x + ((drawRect.x - x) * visibleRect.width) / drawRect.width; 433 visibleRect.y = mousePointInImg.y + ((drawRect.y - y) * visibleRect.height) / drawRect.height; 434 435 // The position is also limited by the image size 436 visibleRect.checkRectPos(); 437 438 synchronized (ImageDisplay.this) { 439 if (ImageDisplay.this.entry == entry) { 440 ImageDisplay.this.visibleRect = visibleRect; 441 } 442 } 443 ImageDisplay.this.repaint(); 444 } 445 446 /** Zoom in and out, trying to preserve the point of the image that was under the mouse cursor 447 * at the same place */ 448 @Override 449 public void mouseWheelMoved(MouseWheelEvent e) { 450 boolean refreshMousePointInImg = false; 451 452 // To avoid issues when the user tries to zoom in on the image borders, this 453 // point is not recalculated as long as e occurs at roughly the same position. 454 if (lastMouseEvent == null || mousePointInImg == null || 455 ((lastMouseEvent.getX()-e.getX())*(lastMouseEvent.getX()-e.getX()) 456 +(lastMouseEvent.getY()-e.getY())*(lastMouseEvent.getY()-e.getY()) > 4*4)) { 457 lastMouseEvent = e; 458 refreshMousePointInImg = true; 459 } 460 461 mouseWheelMovedImpl(e.getX(), e.getY(), e.getWheelRotation(), refreshMousePointInImg); 462 } 463 464 /** Center the display on the point that has been clicked */ 465 @Override 466 public void mouseClicked(MouseEvent e) { 467 // Move the center to the clicked point. 468 ImageEntry entry; 469 Image image; 470 VisRect visibleRect; 471 472 synchronized (ImageDisplay.this) { 473 entry = ImageDisplay.this.entry; 474 image = ImageDisplay.this.image; 475 visibleRect = ImageDisplay.this.visibleRect; 476 } 477 478 if (image == null) 479 return; 480 481 if (ZOOM_ON_CLICK.get()) { 482 // click notions are less coherent than wheel, refresh mousePointInImg on each click 483 lastMouseEvent = null; 484 485 if (mouseIsZoomSelecting(e) && !isAtMaxZoom(visibleRect)) { 486 // zoom in if clicked with the zoom button 487 mouseWheelMovedImpl(e.getX(), e.getY(), -1, true); 488 return; 489 } 490 if (mouseIsDragging(e)) { 491 // zoom out if clicked with the drag button 492 mouseWheelMovedImpl(e.getX(), e.getY(), 1, true); 493 return; 494 } 495 } 496 497 // Calculate the translation to set the clicked point the center of the view. 498 Point click = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize()); 499 Point center = getCenterImgCoord(visibleRect); 500 501 visibleRect.x += click.x - center.x; 502 visibleRect.y += click.y - center.y; 503 504 visibleRect.checkRectPos(); 505 506 synchronized (ImageDisplay.this) { 507 if (ImageDisplay.this.entry == entry) { 508 ImageDisplay.this.visibleRect = visibleRect; 509 } 510 } 511 ImageDisplay.this.repaint(); 512 } 513 514 /** Initialize the dragging, either with button 1 (simple dragging) or button 3 (selection of 515 * a picture part) */ 516 @Override 517 public void mousePressed(MouseEvent e) { 518 Image image; 519 VisRect visibleRect; 520 521 synchronized (ImageDisplay.this) { 522 image = ImageDisplay.this.image; 523 visibleRect = ImageDisplay.this.visibleRect; 524 } 525 526 if (image == null) 527 return; 528 529 selectedRect = null; 530 531 if (mouseIsDragging(e) || mouseIsZoomSelecting(e)) 532 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize()); 533 } 534 535 @Override 536 public void mouseDragged(MouseEvent e) { 537 if (!mouseIsDragging(e) && !mouseIsZoomSelecting(e)) 538 return; 539 540 ImageEntry entry; 541 Image image; 542 VisRect visibleRect; 543 544 synchronized (ImageDisplay.this) { 545 entry = ImageDisplay.this.entry; 546 image = ImageDisplay.this.image; 547 visibleRect = ImageDisplay.this.visibleRect; 548 } 549 550 if (image == null) 551 return; 552 553 if (mouseIsDragging(e) && mousePointInImg != null) { 554 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize()); 555 visibleRect.isDragUpdate = true; 556 visibleRect.x += mousePointInImg.x - p.x; 557 visibleRect.y += mousePointInImg.y - p.y; 558 visibleRect.checkRectPos(); 559 synchronized (ImageDisplay.this) { 560 if (ImageDisplay.this.entry == entry) { 561 ImageDisplay.this.visibleRect = visibleRect; 562 } 563 } 564 ImageDisplay.this.repaint(); 565 } 566 567 if (mouseIsZoomSelecting(e) && mousePointInImg != null) { 568 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize()); 569 visibleRect.checkPointInside(p); 570 VisRect selectedRect = new VisRect( 571 p.x < mousePointInImg.x ? p.x : mousePointInImg.x, 572 p.y < mousePointInImg.y ? p.y : mousePointInImg.y, 573 p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x, 574 p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y, 575 visibleRect); 576 selectedRect.checkRectSize(); 577 selectedRect.checkRectPos(); 578 ImageDisplay.this.selectedRect = selectedRect; 579 ImageDisplay.this.repaint(); 580 } 581 582 } 583 584 @Override 585 public void mouseReleased(MouseEvent e) { 586 ImageEntry entry; 587 Image image; 588 VisRect visibleRect; 589 590 synchronized (ImageDisplay.this) { 591 entry = ImageDisplay.this.entry; 592 image = ImageDisplay.this.image; 593 visibleRect = ImageDisplay.this.visibleRect; 594 } 595 596 if (image == null) 597 return; 598 599 if (mouseIsDragging(e)) { 600 visibleRect.isDragUpdate = false; 601 } 602 603 if (mouseIsZoomSelecting(e) && selectedRect != null) { 604 int oldWidth = selectedRect.width; 605 int oldHeight = selectedRect.height; 606 607 // Check that the zoom doesn't exceed MAX_ZOOM:1 608 if (selectedRect.width < getSize().width / MAX_ZOOM.get()) { 609 selectedRect.width = (int) (getSize().width / MAX_ZOOM.get()); 610 } 611 if (selectedRect.height < getSize().height / MAX_ZOOM.get()) { 612 selectedRect.height = (int) (getSize().height / MAX_ZOOM.get()); 613 } 614 615 // Set the same ratio for the visible rectangle and the display area 616 int hFact = selectedRect.height * getSize().width; 617 int wFact = selectedRect.width * getSize().height; 618 if (hFact > wFact) { 619 selectedRect.width = hFact / getSize().height; 620 } else { 621 selectedRect.height = wFact / getSize().width; 622 } 623 624 // Keep the center of the selection 625 if (selectedRect.width != oldWidth) { 626 selectedRect.x -= (selectedRect.width - oldWidth) / 2; 627 } 628 if (selectedRect.height != oldHeight) { 629 selectedRect.y -= (selectedRect.height - oldHeight) / 2; 630 } 631 632 selectedRect.checkRectSize(); 633 selectedRect.checkRectPos(); 634 } 635 636 synchronized (ImageDisplay.this) { 637 if (entry == ImageDisplay.this.entry) { 638 if (selectedRect == null) { 639 ImageDisplay.this.visibleRect = visibleRect; 640 } else { 641 ImageDisplay.this.visibleRect.setBounds(selectedRect); 642 selectedRect = null; 643 } 644 } 645 } 646 ImageDisplay.this.repaint(); 647 } 648 649 @Override 650 public void mouseEntered(MouseEvent e) { 651 // Do nothing 652 } 653 654 @Override 655 public void mouseExited(MouseEvent e) { 656 // Do nothing 657 } 658 659 @Override 660 public void mouseMoved(MouseEvent e) { 661 // Do nothing 662 } 663 } 664 665 /** 666 * Constructs a new {@code ImageDisplay}. 667 */ 668 public ImageDisplay() { 669 addMouseListener(imgMouseListener); 670 addMouseWheelListener(imgMouseListener); 671 addMouseMotionListener(imgMouseListener); 672 Config.getPref().addPreferenceChangeListener(this); 673 preferenceChanged(null); 674 } 675 676 @Override 677 public void destroy() { 678 removeMouseListener(imgMouseListener); 679 removeMouseWheelListener(imgMouseListener); 680 removeMouseMotionListener(imgMouseListener); 681 Config.getPref().removePreferenceChangeListener(this); 682 } 683 684 /** 685 * Sets a new source image to be displayed by this {@code ImageDisplay}. 686 * @param entry new source image 687 * @since 13220 688 */ 689 public void setImage(ImageEntry entry) { 690 synchronized (this) { 691 this.entry = entry; 692 image = null; 693 errorLoading = false; 694 } 695 repaint(); 696 if (entry != null) { 697 new Thread(new LoadImageRunnable(entry), LoadImageRunnable.class.getName()).start(); 698 } 699 } 700 701 /** 702 * Set the message displayed when there is no image to display. 703 * By default it display a simple No image 704 * @param emptyText the string to display 705 * @since 15333 706 */ 707 public void setEmptyText(String emptyText) { 708 this.emptyText = emptyText; 709 } 710 711 /** 712 * Sets the On-Screen-Display text. 713 * @param text text to display on top of the image 714 */ 715 public void setOsdText(String text) { 716 if (!text.equals(this.osdText)) { 717 this.osdText = text; 718 repaint(); 719 } 720 } 721 722 @Override 723 public void paintComponent(Graphics g) { 724 ImageEntry entry; 725 Image image; 726 VisRect visibleRect; 727 boolean errorLoading; 728 729 synchronized (this) { 730 image = this.image; 731 entry = this.entry; 732 visibleRect = this.visibleRect; 733 errorLoading = this.errorLoading; 734 } 735 736 if (g instanceof Graphics2D) { 737 ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 738 } 739 740 Dimension size = getSize(); 741 if (entry == null) { 742 g.setColor(Color.black); 743 if (emptyText == null) { 744 emptyText = tr("No image"); 745 } 746 String noImageStr = emptyText; 747 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(noImageStr, g); 748 g.drawString(noImageStr, 749 (int) ((size.width - noImageSize.getWidth()) / 2), 750 (int) ((size.height - noImageSize.getHeight()) / 2)); 751 } else if (image == null) { 752 g.setColor(Color.black); 753 String loadingStr; 754 if (!errorLoading) { 755 loadingStr = tr("Loading {0}", entry.getFile().getName()); 756 } else { 757 loadingStr = tr("Error on file {0}", entry.getFile().getName()); 758 } 759 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g); 760 g.drawString(loadingStr, 761 (int) ((size.width - noImageSize.getWidth()) / 2), 762 (int) ((size.height - noImageSize.getHeight()) / 2)); 763 } else { 764 Rectangle r = new Rectangle(visibleRect); 765 Rectangle target = calculateDrawImageRectangle(visibleRect, size); 766 double scale = target.width / (double) r.width; // pixel ratio is 1:1 767 768 if (selectedRect == null && !visibleRect.isDragUpdate && 769 bilinLower < scale && scale < bilinUpper) { 770 try { 771 BufferedImage bi = ImageProvider.toBufferedImage(image, r); 772 if (bi != null) { 773 r.x = r.y = 0; 774 775 // See https://community.oracle.com/docs/DOC-983611 - The Perils of Image.getScaledInstance() 776 // Pre-scale image when downscaling by more than two times to avoid aliasing from default algorithm 777 bi = ImageProvider.createScaledImage(bi, target.width, target.height, 778 RenderingHints.VALUE_INTERPOLATION_BILINEAR); 779 r.width = target.width; 780 r.height = target.height; 781 image = bi; 782 } 783 } catch (OutOfMemoryError oom) { 784 Logging.trace(oom); 785 // fall-back to the non-bilinear scaler 786 r.x = visibleRect.x; 787 r.y = visibleRect.y; 788 } 789 } else { 790 // if target and r cause drawImage to scale image region to a tmp buffer exceeding 791 // its bounds, it will silently fail; crop with r first in such cases 792 // (might be impl. dependent, exhibited by openjdk 1.8.0_151) 793 if (scale*(r.x+r.width) > Short.MAX_VALUE || scale*(r.y+r.height) > Short.MAX_VALUE) { 794 image = ImageProvider.toBufferedImage(image, r); 795 r.x = r.y = 0; 796 } 797 } 798 799 g.drawImage(image, 800 target.x, target.y, target.x + target.width, target.y + target.height, 801 r.x, r.y, r.x + r.width, r.y + r.height, null); 802 803 if (selectedRect != null) { 804 Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y, size); 805 Point bottomRight = img2compCoord(visibleRect, 806 selectedRect.x + selectedRect.width, 807 selectedRect.y + selectedRect.height, size); 808 g.setColor(new Color(128, 128, 128, 180)); 809 g.fillRect(target.x, target.y, target.width, topLeft.y - target.y); 810 g.fillRect(target.x, target.y, topLeft.x - target.x, target.height); 811 g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height); 812 g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y); 813 g.setColor(Color.black); 814 g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y); 815 } 816 if (errorLoading) { 817 String loadingStr = tr("Error on file {0}", entry.getFile().getName()); 818 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g); 819 g.drawString(loadingStr, 820 (int) ((size.width - noImageSize.getWidth()) / 2), 821 (int) ((size.height - noImageSize.getHeight()) / 2)); 822 } 823 if (osdText != null) { 824 FontMetrics metrics = g.getFontMetrics(g.getFont()); 825 int ascent = metrics.getAscent(); 826 Color bkground = new Color(255, 255, 255, 128); 827 int lastPos = 0; 828 int pos = osdText.indexOf('\n'); 829 int x = 3; 830 int y = 3; 831 String line; 832 while (pos > 0) { 833 line = osdText.substring(lastPos, pos); 834 Rectangle2D lineSize = metrics.getStringBounds(line, g); 835 g.setColor(bkground); 836 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight()); 837 g.setColor(Color.black); 838 g.drawString(line, x, y + ascent); 839 y += (int) lineSize.getHeight(); 840 lastPos = pos + 1; 841 pos = osdText.indexOf('\n', lastPos); 842 } 843 844 line = osdText.substring(lastPos); 845 Rectangle2D lineSize = g.getFontMetrics(g.getFont()).getStringBounds(line, g); 846 g.setColor(bkground); 847 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight()); 848 g.setColor(Color.black); 849 g.drawString(line, x, y + ascent); 850 } 851 } 852 } 853 854 static Point img2compCoord(VisRect visibleRect, int xImg, int yImg, Dimension compSize) { 855 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize); 856 return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width, 857 drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height); 858 } 859 860 static Point comp2imgCoord(VisRect visibleRect, int xComp, int yComp, Dimension compSize) { 861 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize); 862 Point p = new Point( 863 ((xComp - drawRect.x) * visibleRect.width), 864 ((yComp - drawRect.y) * visibleRect.height)); 865 p.x += (((p.x % drawRect.width) << 1) >= drawRect.width) ? drawRect.width : 0; 866 p.y += (((p.y % drawRect.height) << 1) >= drawRect.height) ? drawRect.height : 0; 867 p.x = visibleRect.x + p.x / drawRect.width; 868 p.y = visibleRect.y + p.y / drawRect.height; 869 return p; 870 } 871 872 static Point getCenterImgCoord(Rectangle visibleRect) { 873 return new Point(visibleRect.x + visibleRect.width / 2, 874 visibleRect.y + visibleRect.height / 2); 875 } 876 877 static VisRect calculateDrawImageRectangle(VisRect visibleRect, Dimension compSize) { 878 return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, compSize.width, compSize.height)); 879 } 880 881 /** 882 * calculateDrawImageRectangle 883 * 884 * @param imgRect the part of the image that should be drawn (in image coordinates) 885 * @param compRect the part of the component where the image should be drawn (in component coordinates) 886 * @return the part of compRect with the same width/height ratio as the image 887 */ 888 static VisRect calculateDrawImageRectangle(VisRect imgRect, Rectangle compRect) { 889 int x = 0; 890 int y = 0; 891 int w = compRect.width; 892 int h = compRect.height; 893 894 int wFact = w * imgRect.height; 895 int hFact = h * imgRect.width; 896 if (wFact != hFact) { 897 if (wFact > hFact) { 898 w = hFact / imgRect.height; 899 x = (compRect.width - w) / 2; 900 } else { 901 h = wFact / imgRect.width; 902 y = (compRect.height - h) / 2; 903 } 904 } 905 906 // overscan to prevent empty edges when zooming in to zoom scales > 2:1 907 if (w > imgRect.width && h > imgRect.height && !imgRect.isFullView1D() && wFact != hFact) { 908 if (wFact > hFact) { 909 w = compRect.width; 910 x = 0; 911 h = wFact / imgRect.width; 912 y = (compRect.height - h) / 2; 913 } else { 914 h = compRect.height; 915 y = 0; 916 w = hFact / imgRect.height; 917 x = (compRect.width - w) / 2; 918 } 919 } 920 921 return new VisRect(x + compRect.x, y + compRect.y, w, h, imgRect); 922 } 923 924 /** 925 * Make the current image either scale to fit inside this component, 926 * or show a portion of image (1:1), if the image size is larger than 927 * the component size. 928 */ 929 public void zoomBestFitOrOne() { 930 ImageEntry entry; 931 Image image; 932 VisRect visibleRect; 933 934 synchronized (this) { 935 entry = this.entry; 936 image = this.image; 937 visibleRect = this.visibleRect; 938 } 939 940 if (image == null) 941 return; 942 943 if (visibleRect.width != image.getWidth(null) || visibleRect.height != image.getHeight(null)) { 944 // The display is not at best fit. => Zoom to best fit 945 visibleRect.reset(); 946 } else { 947 // The display is at best fit => zoom to 1:1 948 Point center = getCenterImgCoord(visibleRect); 949 visibleRect.setBounds(center.x - getWidth() / 2, center.y - getHeight() / 2, 950 getWidth(), getHeight()); 951 visibleRect.checkRectSize(); 952 visibleRect.checkRectPos(); 953 } 954 955 synchronized (this) { 956 if (this.entry == entry) { 957 this.visibleRect = visibleRect; 958 } 959 } 960 repaint(); 961 } 962}