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