Package: EditableImageRenderer
EditableImageRenderer
name | instruction | branch | complexity | line | method | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
EditableImageRenderer() |
|
|
|
|
|
||||||||||||||||||||
addImageRendererListener(EditableImageRendererListener) |
|
|
|
|
|
||||||||||||||||||||
addOverlay(Overlay) |
|
|
|
|
|
||||||||||||||||||||
centerImage() |
|
|
|
|
|
||||||||||||||||||||
computeCenterPoint() |
|
|
|
|
|
||||||||||||||||||||
computeOrigin(Point, Point, double) |
|
|
|
|
|
||||||||||||||||||||
convertImagePointToComponentPoint(Point) |
|
|
|
|
|
||||||||||||||||||||
fireAngleChangedEvent() |
|
|
|
|
|
||||||||||||||||||||
fireEditingToolActivated(EditingTool) |
|
|
|
|
|
||||||||||||||||||||
fireEditingToolDeactivated(EditingTool) |
|
|
|
|
|
||||||||||||||||||||
fireScaleChangedEvent() |
|
|
|
|
|
||||||||||||||||||||
fitToDisplaySize() |
|
|
|
|
|
||||||||||||||||||||
flushAllCaches() |
|
|
|
|
|
||||||||||||||||||||
flushScaledImageCache() |
|
|
|
|
|
||||||||||||||||||||
getAngle() |
|
|
|
|
|
||||||||||||||||||||
getAvailableHeight() |
|
|
|
|
|
||||||||||||||||||||
getAvailableWidth() |
|
|
|
|
|
||||||||||||||||||||
getEditingTool() |
|
|
|
|
|
||||||||||||||||||||
getFitScale() |
|
|
|
|
|
||||||||||||||||||||
getImage() |
|
|
|
|
|
||||||||||||||||||||
getImageBorder() |
|
|
|
|
|
||||||||||||||||||||
getMargin() |
|
|
|
|
|
||||||||||||||||||||
getMaxScale() |
|
|
|
|
|
||||||||||||||||||||
getMinScale() |
|
|
|
|
|
||||||||||||||||||||
getOptimizedImage() |
|
|
|
|
|
||||||||||||||||||||
getOrigin() |
|
|
|
|
|
||||||||||||||||||||
getPositionOverImage(Point) |
|
|
|
|
|
||||||||||||||||||||
getPreviewSettings() |
|
|
|
|
|
||||||||||||||||||||
getRotateQuality() |
|
|
|
|
|
||||||||||||||||||||
getScale() |
|
|
|
|
|
||||||||||||||||||||
getScaleQuality() |
|
|
|
|
|
||||||||||||||||||||
internalSetOrigin(Point) |
|
|
|
|
|
||||||||||||||||||||
internalSetScale(double) |
|
|
|
|
|
||||||||||||||||||||
isOptimizedImageEnabled() |
|
|
|
|
|
||||||||||||||||||||
isRepaintEnabled() |
|
|
|
|
|
||||||||||||||||||||
isScaledImageCachingEnabled() |
|
|
|
|
|
||||||||||||||||||||
isScrollBarsVisible() |
|
|
|
|
|
||||||||||||||||||||
lambda$new$0(AdjustmentEvent) |
|
|
|
|
|
||||||||||||||||||||
layoutScrollBars() |
|
|
|
|
|
||||||||||||||||||||
moveOrigin(int, int) |
|
|
|
|
|
||||||||||||||||||||
paint(Graphics) |
|
|
|
|
|
||||||||||||||||||||
removeImageRendererListener(EditableImageRendererListener) |
|
|
|
|
|
||||||||||||||||||||
removeOverlay(Overlay) |
|
|
|
|
|
||||||||||||||||||||
setAngle(double) |
|
|
|
|
|
||||||||||||||||||||
setClippingShape(Shape) |
|
|
|
|
|
||||||||||||||||||||
setFitToDisplaySize(boolean) |
|
|
|
|
|
||||||||||||||||||||
setImage(EditableImage) |
|
|
|
|
|
||||||||||||||||||||
setImageBorder(Border) |
|
|
|
|
|
||||||||||||||||||||
setMargin(Insets) |
|
|
|
|
|
||||||||||||||||||||
setMaxScale(double) |
|
|
|
|
|
||||||||||||||||||||
setMinScale(double) |
|
|
|
|
|
||||||||||||||||||||
setOptimizedImageEnabled(boolean) |
|
|
|
|
|
||||||||||||||||||||
setOrigin(Point) |
|
|
|
|
|
||||||||||||||||||||
setPositionOverImage(Point, Point) |
|
|
|
|
|
||||||||||||||||||||
setPreviewSettings(PreviewSettings) |
|
|
|
|
|
||||||||||||||||||||
setRepaintEnabled(boolean) |
|
|
|
|
|
||||||||||||||||||||
setRotateQuality(Quality) |
|
|
|
|
|
||||||||||||||||||||
setScale(double) |
|
|
|
|
|
||||||||||||||||||||
setScale(double, Point) |
|
|
|
|
|
||||||||||||||||||||
setScaleQuality(Quality) |
|
|
|
|
|
||||||||||||||||||||
setScaledImageCachingEnabled(boolean) |
|
|
|
|
|
||||||||||||||||||||
setScrollBarsVisible(boolean) |
|
|
|
|
|
||||||||||||||||||||
static {...} |
|
|
|
|
|
||||||||||||||||||||
update(Graphics) |
|
|
|
|
|
||||||||||||||||||||
updateScrollBars() |
|
|
|
|
|
Coverage
1: /*
2: * *********************************************************************************************************************
3: *
4: * Mistral: open source imaging engine
5: * http://tidalwave.it/projects/mistral
6: *
7: * Copyright (C) 2003 - 2023 by Tidalwave s.a.s. (http://tidalwave.it)
8: *
9: * *********************************************************************************************************************
10: *
11: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
12: * the License. You may obtain a copy of the License at
13: *
14: * http://www.apache.org/licenses/LICENSE-2.0
15: *
16: * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
17: * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
18: * specific language governing permissions and limitations under the License.
19: *
20: * *********************************************************************************************************************
21: *
22: * git clone https://bitbucket.org/tidalwave/mistral-src
23: * git clone https://github.com/tidalwave-it/mistral-src
24: *
25: * *********************************************************************************************************************
26: */
27: package it.tidalwave.image.render;
28:
29: import java.util.ArrayList;
30: import java.util.List;
31: import java.awt.Color;
32: import java.awt.Graphics;
33: import java.awt.Graphics2D;
34: import java.awt.Insets;
35: import java.awt.Point;
36: import java.awt.Shape;
37: import java.awt.event.AdjustmentListener;
38: import javax.swing.JComponent;
39: import javax.swing.JPanel;
40: import javax.swing.JScrollBar;
41: import javax.swing.border.Border;
42: import it.tidalwave.image.EditableImage;
43: import it.tidalwave.image.Quality;
44: import it.tidalwave.image.op.OptimizeOp;
45: import it.tidalwave.image.op.PaintOp;
46: import it.tidalwave.image.op.RotateOp;
47: import it.tidalwave.image.op.ScaleOp;
48: import it.tidalwave.image.render.event.EditableImageRendererEvent;
49: import it.tidalwave.image.render.event.EditableImageRendererListener;
50: import it.tidalwave.image.util.Platform;
51: import lombok.extern.slf4j.Slf4j;
52:
53: /***********************************************************************************************************************
54: *
55: * This class is a pipe which adds to SimpleEditableImageRenderer scrolling
56: * capabilities and a fit-to-size feature.
57: *
58: * @author Fabrizio Giudici
59: *
60: **********************************************************************************************************************/
61: @Slf4j
62: public class EditableImageRenderer extends JComponent
63: {
64: /**
65: * The maximum allowed value for scale.
66: */
67: public static final double MAX_SCALE = 40;
68:
69: /**
70: * The maximum allowed value for scale.
71: */
72: public static final double MIN_SCALE = 0.01;
73:
74: /**
75: * Over this image size the scaled image caching is always disabled.
76: */
77: private static final int MAX_SIZE_FOR_SCALED_CACHING = 8000;
78:
79: /**
80: * The default background color for parts not covered by the image.
81: */
82: private static final Color DEFAULT_BACKGROUND = Color.DARK_GRAY;
83:
84: /**
85: * An empty margin.
86: */
87: private static final Insets NULL_MARGIN = new Insets(0, 0, 0, 0);
88:
89: /**
90: * The original image to be displayed.
91: */
92: protected EditableImage image;
93:
94: /**
95: * A display-optimized copy of the image. If optimizedImageEnabled is false, it just references the original image.
96: */
97: private EditableImage optimizedImage;
98:
99: /**
100: * A scaled-down version of the image to fit the actual rendering settings.
101: */
102: private EditableImage scaledImage;
103:
104: /**
105: * True if a local optimized copy of the image should be used.
106: */
107: private boolean optimizedImageEnabled;
108:
109: /**
110: * True if use a scaled local copy of the image for faster rendering.
111: */
112: private boolean scaledImageCachingEnabled;
113:
114: /**
115: * The current scale.
116: */
117: protected double scale = 1;
118:
119: private double minScale = MIN_SCALE;
120:
121: private double maxScale = MAX_SCALE;
122:
123: /**
124: * The current rotation.
125: */
126: protected double angle = 0;
127:
128: /**
129: * The image coordinates of the pixel shown in the top left corner of the component.
130: */
131: private Point origin = new Point(0, 0);
132:
133: /**
134: * The current preview settings.
135: */
136: private PreviewSettings previewSettings;
137:
138: /**
139: * The coordinates of the photo origin relative to the component location.
140: */
141: private int shownImageX;
142:
143: /**
144: * The coordinates of the photo origin relative to the component location.
145: */
146: private int shownImageY;
147:
148: /**
149: * The scaled photo dimension in pixels.
150: */
151: private int shownImageWidth;
152:
153: /**
154: * The scaled photo dimension in pixels.
155: */
156: private int shownImageHeight;
157:
158: /**
159: * The maximum margin that can be shown around the image.
160: */
161: private Insets margin = new Insets(0, 0, 0, 0);
162:
163: /**
164: * If not null, the image rendering will be clipped against this shape.
165: */
166: private Shape clippingShape;
167:
168: /**
169: * The quality used for scale.
170: */
171: private Quality scaleQuality = Quality.INTERMEDIATE;
172:
173: /**
174: * The quality used for rotate.
175: */
176: private Quality rotateQuality = Quality.INTERMEDIATE;
177:
178: /**
179: * Overlays will be drawn over the image.
180: */
181: private final List<Overlay> overlayList = new ArrayList<>();
182:
183: /**
184: * If true, the image always fits the component size.
185: */
186: private boolean fitToDisplaySize;
187:
188: /**
189: * The list of listeners.
190: */
191: private final List<EditableImageRendererListener> listenerList = new ArrayList<>();
192:
193: /**
194: * True if repaint is currently enabled.
195: */
196: private boolean repaintEnabled = true;
197:
198: /**
199: * The current EditingTool.
200: */
201: protected EditingTool editingTool;
202:
203: /**
204: * The vertical scrollbar.
205: */
206: private final JScrollBar horizontalScrollBar = new JScrollBar(JScrollBar.HORIZONTAL);
207:
208: /**
209: * The horizontal scrollbar.
210: */
211: private final JScrollBar verticalScrollBar = new JScrollBar(JScrollBar.VERTICAL);
212:
213: private final JPanel filler = new JPanel();
214:
215: /**
216: * True if scrollbars should be visible.
217: */
218: private boolean scrollBarsVisible = false;
219:
220: /**
221: * A border to be rendered around the image.
222: */
223: private Border imageBorder;
224:
225: /**
226: * Width of the renderer before the latest resize, used to layout scrollbars.
227: */
228: private int previousWidth;
229:
230: /**
231: * Height of the renderer before the latest resize, used to layout scrollbars.
232: */
233: private int previohsHeight;
234:
235: /**
236: * The thickness of scrollbars.
237: */
238: private int scrollbarThickness = 16;
239:
240: /*******************************************************************************************************************
241: *
242: * The scrollbar listener.
243: *
244: ******************************************************************************************************************/
245: private final AdjustmentListener scrollbarListener =
246: event -> setOrigin(new Point(horizontalScrollBar.getValue(), verticalScrollBar.getValue()));
247:
248: /*******************************************************************************************************************
249: *
250: *
251: ******************************************************************************************************************/
252: public EditableImageRenderer()
253: {
254: setBackground(DEFAULT_BACKGROUND);
255: setLayout(null);
256: setOpaque(false);
257:
258:• final var workaroundMST63 = !Platform.isMacOSX();
259:
260:• if (workaroundMST63)
261: {
262: log.warn("Enabled workaround for MST-63");
263: }
264:
265: setScaledImageCachingEnabled(workaroundMST63);
266: setOptimizedImageEnabled(workaroundMST63);
267:
268: add(horizontalScrollBar);
269: add(verticalScrollBar);
270: add(filler);
271: horizontalScrollBar.addAdjustmentListener(scrollbarListener);
272: verticalScrollBar.addAdjustmentListener(scrollbarListener);
273: horizontalScrollBar.setVisible(scrollBarsVisible);
274: verticalScrollBar.setVisible(scrollBarsVisible);
275: filler.setVisible(scrollBarsVisible);
276: }
277:
278: /*******************************************************************************************************************
279: *
280: * Sets the image to display. The image is internally cloned, so any further
281: * operation performed on the same source (that could cause a model switch) won't
282: * affect the rendering.
283: *
284: * @param image the image
285: *
286: ******************************************************************************************************************/
287: public void setImage (final EditableImage image)
288: {
289: log.info("setImage(" + image + ")");
290:
291:• if (image == null)
292: {
293: this.image = null;
294: log.warn("setImage(null)");
295: }
296:
297: else
298: {
299: this.image = image; // image.cloneImage();
300: }
301:
302:• if (editingTool != null)
303: {
304: editingTool.imageChanged();
305: }
306:
307: flushAllCaches();
308: updateScrollBars();
309:
310:• if (fitToDisplaySize)
311: {
312: fitToDisplaySize();
313: }
314: else
315: {
316: repaint();
317: }
318: }
319:
320: /*******************************************************************************************************************
321: *
322: * Returns the displayed image.
323: *
324: * @return the image
325: *
326: ******************************************************************************************************************/
327: public EditableImage getImage()
328: {
329: return image;
330: }
331:
332: /*******************************************************************************************************************
333: *
334: * Returns a possibly optimized version of the image. If useOptimizedImage is
335: * false, this method returns the original image.
336: *
337: * @return the image
338: *
339: ******************************************************************************************************************/
340:
341: // useful for the loupe, to prevent it from recomputing the optimized version
342: public EditableImage getOptimizedImage()
343: {
344: return optimizedImage;
345: }
346:
347: /*******************************************************************************************************************
348: *
349: * Turns on/off repaint. It's advisable to turn repainting off before a sequence
350: * of operations, and turning it on again only at the end of the sequence.
351: *
352: * @param repaintEnabled the new setting
353: *
354: ******************************************************************************************************************/
355: public void setRepaintEnabled (final boolean repaintEnabled)
356: {
357: log.info("setRepaintEnabled(" + repaintEnabled + ")");
358: this.repaintEnabled = repaintEnabled;
359: }
360:
361: /*******************************************************************************************************************
362: *
363: * Returns the state of repaint.
364: *
365: * @return the repaint state
366: *
367: ******************************************************************************************************************/
368: public boolean isRepaintEnabled()
369: {
370: return repaintEnabled;
371: }
372:
373: /*******************************************************************************************************************
374: *
375: * Sets the image point which is displayed in the top left corner (coordinates
376: * are in actual image pixels).
377: *
378: * @param origin the origin
379: *
380: ******************************************************************************************************************/
381: public void setOrigin (final Point origin)
382: {
383: log.info("setOrigin(" + origin + ")");
384:
385:• if ((image != null) && (image.getWidth() > 0) && (image.getHeight() > 0))
386: {
387: internalSetOrigin(origin);
388: updateScrollBars();
389: repaint();
390: }
391: }
392:
393: private void internalSetOrigin (final Point origin)
394: {
395: log.info("internalSetOrigin(" + origin + ")");
396: //
397: // No margin with the scroll bars.
398: //
399: // final Insets margin = scrollBarsVisible ? NULL_MARGIN : this.margin;
400: //
401: // The size of the largest image displayable with no clipping at the current zoom.
402: //
403: final var maxWidth = (int)Math.round(getAvailableWidth() / scale);
404: final var maxHeight = (int)Math.round(getAvailableHeight() / scale);
405:
406: //
407: // The size of the image including the margin.
408: //
409: final var widthWithMargin = image.getWidth() + margin.left + margin.right;
410: final var heightWithMargin = image.getHeight() + margin.top + margin.bottom;
411:
412: //
413: // The bounds for the origin to keep the margin within its bounds.
414: //
415: final var xMin = -margin.left;
416: final var yMin = -margin.top;
417: final var xMax = (image.getWidth() + margin.right) - maxWidth;
418: final var yMax = (image.getHeight() + margin.bottom) - maxHeight;
419: //
420: // If there's more room to display the image with its margin, center it.
421: //
422:• this.origin.x = (maxWidth <= widthWithMargin)
423: ? Math.min(Math.max(xMin, origin.x), xMax)
424: : (-(maxWidth - image.getWidth()) / 2);
425:• this.origin.y = (maxHeight <= heightWithMargin)
426: ? Math.min(Math.max(yMin, origin.y), yMax)
427: : (-(maxHeight - image.getHeight()) / 2);
428: }
429:
430: /*******************************************************************************************************************
431: *
432: *
433: ******************************************************************************************************************/
434: public Point getOrigin()
435: {
436: return origin;
437: }
438:
439: /*******************************************************************************************************************
440: *
441: *
442: ******************************************************************************************************************/
443: public double getScale()
444: {
445: return scale;
446: }
447:
448: /*******************************************************************************************************************
449: *
450: *
451: ******************************************************************************************************************/
452: public void setAngle (final double angle)
453: {
454: log.info("setAngle(" + angle + ")");
455:
456:• if (this.angle != angle)
457: {
458: this.angle = angle;
459: flushScaledImageCache();
460: repaint();
461: fireAngleChangedEvent();
462: }
463: }
464:
465: /*******************************************************************************************************************
466: *
467: *
468: ******************************************************************************************************************/
469: public double getAngle()
470: {
471: return angle;
472: }
473:
474: /*******************************************************************************************************************
475: *
476: *
477: ******************************************************************************************************************/
478: public EditingTool getEditingTool()
479: {
480: return editingTool;
481: }
482:
483: /*******************************************************************************************************************
484: *
485: *
486: ******************************************************************************************************************/
487: public void setImageBorder (final Border imageBorder)
488: {
489: this.imageBorder = imageBorder;
490: }
491:
492: /*******************************************************************************************************************
493: *
494: *
495: ******************************************************************************************************************/
496: public Border getImageBorder()
497: {
498: return imageBorder;
499: }
500:
501: /*******************************************************************************************************************
502: *
503: * Given a point in component coordinates, returns the coordinates of the
504: * image pixel rendered at that point. If the point is outside of the image
505: * rendering areas, returns null.
506: *
507: * @param componentPoint the point in relative coordinates
508: * @return the image pixel coordinates (null if none)
509: *
510: ******************************************************************************************************************/
511: public Point getPositionOverImage (final Point componentPoint)
512: {
513:• if (image == null)
514: {
515: return null;
516: }
517:
518: final var imageWidth = image.getWidth();
519: final var imageHeight = image.getHeight();
520:
521:• if ((imageWidth == 0) || (imageHeight == 0)) // can happen if metadata is not loaded yet
522: {
523: return null;
524: }
525:
526:• if ((shownImageWidth == 0) || (shownImageHeight == 0))
527: {
528: log.error("Image size: " + shownImageHeight + " x " + shownImageHeight);
529: return null;
530: }
531:
532: final var x = ((componentPoint.x - shownImageX) * imageWidth) / shownImageWidth;
533: final var y = ((componentPoint.y - shownImageY) * imageHeight) / shownImageHeight;
534:
535:• if ((x >= 0) && (y >= 0) && (x < imageWidth) && (y < imageHeight))
536: {
537: return new Point(x, y);
538: }
539:
540: else
541: {
542: return null;
543: }
544: }
545:
546: /*******************************************************************************************************************
547: *
548: * Given a point in image coordinates, returns the coordinates of the
549: * component point which renders that image point.
550: *
551: * @param imagePoint the point in image coordinates
552: * @return the point coordinates
553: *
554: ******************************************************************************************************************/
555: public Point convertImagePointToComponentPoint (final Point imagePoint)
556: {
557:• if (image == null)
558: {
559: return null;
560: }
561:
562: final var imageWidth = image.getWidth();
563: final var imageHeight = image.getHeight();
564:
565:• if ((imageWidth == 0) || (imageHeight == 0)) // can happen if metadata is not loaded yet
566: {
567: return null;
568: }
569:
570:• if ((shownImageWidth == 0) || (shownImageHeight == 0))
571: {
572: log.error("Image size: " + shownImageHeight + " x " + shownImageHeight);
573:
574: return null;
575: }
576:
577: final var x = ((imagePoint.x * shownImageWidth) / imageWidth) + shownImageX;
578: final var y = ((imagePoint.y * shownImageHeight) / imageHeight) + shownImageY;
579:
580: return new Point(x, y);
581:
582: /* if ((x >= 0) && (y >= 0) && (x < imageWidth) && (y < imageHeight))
583: {
584: return new Point(x, y);
585: }
586:
587: else
588: {
589: return null;
590: }*/
591: }
592:
593: /*******************************************************************************************************************
594: *
595: * Ensures that the given image pixel is shown at the given component
596: * coordinates.
597: *
598: * @param imagePoint the coordinates of the image pixel
599: * @param componentPoint the relative coordinates where to show imagePoint
600: *
601: ******************************************************************************************************************/
602: public void setPositionOverImage (final Point imagePoint, final Point componentPoint)
603: {
604: log.info("setPositionOverImage(" + imagePoint + ", " + componentPoint + ")");
605: final var newOrigin = computeOrigin(imagePoint, componentPoint, scale);
606:
607:• if (newOrigin != null)
608: {
609: setOrigin(newOrigin);
610: }
611: }
612:
613: /*******************************************************************************************************************
614: *
615: * Sets the maximum margin that can be shown around the image. The number of
616: * pixels is in image scale.
617: *
618: * Please note that the margin is ignored when the scroll bars are visible.
619: *
620: * @param margin the new margin
621: *
622: ******************************************************************************************************************/
623: public void setMargin (final Insets margin)
624: {
625: log.info("setMargin(" + margin + ")");
626: this.margin = (Insets)margin.clone();
627: }
628:
629: /*******************************************************************************************************************
630: *
631: * Returns the maximum margin that can be shown around the image.
632: *
633: * @return the margin
634: *
635: ******************************************************************************************************************/
636: public Insets getMargin()
637: {
638: return (Insets)margin.clone();
639: }
640:
641: /*******************************************************************************************************************
642: *
643: * Sets the scrollbars visible or not.
644: *
645: * @param scrollBarsVisible the new setting
646: *
647: ******************************************************************************************************************/
648: public void setScrollBarsVisible (final boolean scrollBarsVisible)
649: {
650: log.info("setScrollBarsVisible(" + scrollBarsVisible + ")");
651:
652:• if (this.scrollBarsVisible != scrollBarsVisible)
653: {
654: this.scrollBarsVisible = scrollBarsVisible;
655:
656:• if (scrollBarsVisible)
657: {
658: previousWidth = previohsHeight = -1;
659: updateScrollBars();
660: }
661:
662: horizontalScrollBar.setVisible(scrollBarsVisible);
663: verticalScrollBar.setVisible(scrollBarsVisible);
664: filler.setVisible(scrollBarsVisible);
665: repaint();
666: }
667: }
668:
669: /*******************************************************************************************************************
670: *
671: * Return true if the scrollbars are visible.
672: *
673: * @return true if the scrollbars are visible
674: *
675: ******************************************************************************************************************/
676: public boolean isScrollBarsVisible()
677: {
678: return scrollBarsVisible;
679: }
680:
681: /*******************************************************************************************************************
682: *
683: * Computes a new origin so that the given image point is shown at the given
684: * relative coordinates.
685: *
686: * @param imagePoint the coordinates of the image pixel
687: * @param componentPoint the relative coordinates where to show imagePoint
688: * @return the new origin
689: *
690: ******************************************************************************************************************/
691: protected Point computeOrigin (final Point imagePoint, final Point componentPoint, final double scale)
692: {
693:• if (image == null)
694: {
695: return null;
696: }
697:
698: final var imageWidth = image.getWidth();
699: final var imageHeight = image.getHeight();
700:
701:• if ((imageWidth == 0) || (imageHeight == 0)) // can happen if metadata is not loaded yet
702: {
703: return null;
704: }
705:
706: return new Point((int)Math.round(imagePoint.x - (componentPoint.x / scale)),
707: (int)Math.round(imagePoint.y - (componentPoint.y / scale)));
708: }
709:
710: /*******************************************************************************************************************
711: *
712: * Sets the quality used for scale operations. This operation doesn't force a
713: * <code>repaint()</code>, so it must be explicitly invoked if you want to see
714: * immediately the quality change.
715: *
716: * @param quality the quality
717: *
718: ******************************************************************************************************************/
719: public void setScaleQuality (final Quality scaleQuality)
720: {
721: log.info("setScaleQuality(" + scaleQuality + ")");
722:
723:• if (this.scaleQuality != scaleQuality)
724: {
725: this.scaleQuality = scaleQuality;
726: flushScaledImageCache();
727: }
728: }
729:
730: /*******************************************************************************************************************
731: *
732: * Returns the quality used for scale operations.
733: *
734: * @return the quality
735: *
736: ******************************************************************************************************************/
737: public Quality getScaleQuality()
738: {
739: return scaleQuality;
740: }
741:
742: /*******************************************************************************************************************
743: *
744: * Sets the quality used for rotate operations. This operation doesn't force a
745: * <code>repaint()</code>, so it must be explicitly invoked if you want to see
746: * immediately the quality change.
747: *
748: * @param quality the quality
749: *
750: ******************************************************************************************************************/
751: public void setRotateQuality (final Quality rotateQuality)
752: {
753: log.info("setRotateQuality(" + rotateQuality + ")");
754:
755:• if (this.rotateQuality != rotateQuality)
756: {
757: this.rotateQuality = rotateQuality;
758: flushScaledImageCache();
759: }
760: }
761:
762: /*******************************************************************************************************************
763: *
764: * Returns the quality used for scale operations.
765: *
766: * @return the quality
767: *
768: ******************************************************************************************************************/
769: public Quality getRotateQuality()
770: {
771: return rotateQuality;
772: }
773:
774: /*******************************************************************************************************************
775: *
776: * Enables or disables the caching of a scaled image for faster speed.
777: *
778: * @param cacheScaleImageEnabled the switch for this property
779: *
780: ******************************************************************************************************************/
781: public void setScaledImageCachingEnabled (final boolean scaledImageCachingEnabled)
782: {
783: log.info("setScaledImageCachingEnabled(" + scaledImageCachingEnabled + ")");
784: this.scaledImageCachingEnabled = scaledImageCachingEnabled;
785:
786:• if (!scaledImageCachingEnabled)
787: {
788: flushScaledImageCache();
789: }
790: }
791:
792: /*******************************************************************************************************************
793: *
794: * Returns the status of the caching of a scaled image for faster speed.
795: *
796: * @return the status of this feature
797: *
798: ******************************************************************************************************************/
799: public boolean isScaledImageCachingEnabled()
800: {
801: return scaledImageCachingEnabled;
802: }
803:
804: /*******************************************************************************************************************
805: *
806: * Enables or disables the use of an optimized copy of the image.
807: *
808: * @param optimizedImageEnabled the switch for this property
809: *
810: ******************************************************************************************************************/
811: public void setOptimizedImageEnabled (final boolean optimizedImageEnabled)
812: {
813: log.info("setOptimizedImageEnabled(" + optimizedImageEnabled + ")");
814: this.optimizedImageEnabled = optimizedImageEnabled;
815: }
816:
817: /*******************************************************************************************************************
818: *
819: * Returns the status of the rgb image caching feature.
820: *
821: * @return the status of this feature
822: *
823: ******************************************************************************************************************/
824: public boolean isOptimizedImageEnabled()
825: {
826: return optimizedImageEnabled;
827: }
828:
829: /*******************************************************************************************************************
830: *
831: * Sets a shape to clip rendering against.
832: *
833: * @param clippingShape the clipping shape
834: *
835: ******************************************************************************************************************/
836: public void setClippingShape (final Shape clippingShape)
837: {
838: this.clippingShape = clippingShape;
839: }
840:
841: /*******************************************************************************************************************
842: *
843: * Adds an overlay to be shown over the image.
844: *
845: * @param overlay the overlay
846: *
847: ******************************************************************************************************************/
848: public void addOverlay (final Overlay overlay)
849: {
850: overlayList.add(overlay);
851: }
852:
853: /*******************************************************************************************************************
854: *
855: *
856: ******************************************************************************************************************/
857: public void removeOverlay (final Overlay overlay)
858: {
859: overlayList.remove(overlay);
860: }
861:
862: /*******************************************************************************************************************
863: *
864: * Sets the preview settings.
865: *
866: ******************************************************************************************************************/
867: public void setPreviewSettings (final PreviewSettings previewSettings)
868: {
869: this.previewSettings = previewSettings;
870: repaint();
871: }
872:
873: /*******************************************************************************************************************
874: *
875: * Gets the preview settings.
876: *
877: ******************************************************************************************************************/
878: public PreviewSettings getPreviewSettings()
879: {
880: return previewSettings;
881: }
882:
883: /*******************************************************************************************************************
884: *
885: *
886: ******************************************************************************************************************/
887: @Override
888: public void update (final Graphics g)
889: {
890: paint(g); // don't waste time on the background
891: }
892:
893: /*******************************************************************************************************************
894: *
895: * Renders this component.
896: *
897: ******************************************************************************************************************/
898: @Override
899: public void paint (final Graphics g)
900: {
901: log.info("paint()");
902:
903:• if (!repaintEnabled)
904: {
905: return;
906: }
907:
908:• if (fitToDisplaySize)
909: {
910: internalSetScale(getFitScale());
911: internalSetOrigin(computeCenterPoint());
912: // fitToDisplaySize();
913: }
914:
915: layoutScrollBars();
916:
917: final var myWidth = getWidth();
918: final var myHeight = getHeight();
919: //if (image == null) // FIXME: this can be optimized
920: {
921: g.setColor(getBackground());
922: g.fillRect(0, 0, myWidth, myHeight);
923: }
924:
925: Graphics2D g2 = null;
926:
927: try
928: {
929:• if (image != null)
930: {
931: EditableImage imageToDraw = null;
932: final var maxSize = scale * Math.max(image.getWidth(), image.getHeight());
933: var needScaling = true;
934: var rotationDeltaX = 0;
935: var rotationDeltaY = 0;
936:
937: //
938: // If scaled image caching is enabled, create a scaled image and then
939: // render it 1:1. Don't use a scaled image if it is too big!
940: //
941:• if (((scaledImageCachingEnabled && (maxSize < MAX_SIZE_FOR_SCALED_CACHING)) || (angle != 0)))
942: {
943:• if (scaledImage == null)
944: {
945: log.debug(">>>> computing scaled image");
946: scaledImage = optimizedImage.execute(new ScaleOp(scale, getScaleQuality()));
947:
948: final var prevWidth = scaledImage.getWidth();
949: final var prevHeight = scaledImage.getHeight();
950: scaledImage.executeInPlace(new RotateOp(angle, getRotateQuality()));
951: //
952: // Rotating the image could have make it bigger (to avoid truncation).
953: // Adjust the origin in order to compensate for it.
954: //
955: rotationDeltaX = (prevWidth - scaledImage.getWidth()) / 2;
956: rotationDeltaY = (prevHeight - scaledImage.getHeight()) / 2;
957: }
958:
959: imageToDraw = scaledImage;
960: shownImageWidth = scaledImage.getWidth();
961: shownImageHeight = scaledImage.getHeight();
962: needScaling = false;
963: }
964:
965: //
966: // Otherwise scale the image on-the-fly.
967: //
968: else
969: {
970: imageToDraw = optimizedImage;
971: shownImageWidth = (int)Math.round(scale * optimizedImage.getWidth());
972: shownImageHeight = (int)Math.round(scale * optimizedImage.getHeight());
973: }
974:
975: shownImageX = -(int)Math.round(scale * origin.x) + rotationDeltaX;
976: shownImageY = -(int)Math.round(scale * origin.y) + rotationDeltaY;
977:
978: g2 = (Graphics2D)g.create(); // make a copy since you're changing hints
979:
980:• if (clippingShape != null)
981: {
982: g2.clip(clippingShape);
983: }
984:
985: //
986: // Don't pass 'this' as an observer, it could trigger paint() twice (FIXME: check if it's true)
987: //
988:• final var paintOp = needScaling
989: ? new PaintOp(g2,
990: shownImageX,
991: shownImageY,
992: shownImageWidth,
993: shownImageHeight,
994: scaleQuality,
995: previewSettings,
996: null)
997: : new PaintOp(g2, shownImageX, shownImageY, previewSettings, null);
998:
999: imageToDraw.executeInPlace(paintOp);
1000:
1001:• if (imageBorder != null)
1002: {
1003: imageBorder.paintBorder(this, g2, shownImageX, shownImageY, shownImageWidth, shownImageHeight);
1004: }
1005: } // if image != null
1006:
1007:• if (g2 == null)
1008: {
1009: g2 = (Graphics2D)g;
1010: }
1011:
1012:• for (final var overlay : overlayList)
1013: {
1014:• if (overlay.isVisible())
1015: {
1016: final var g2Copy = (Graphics2D)g2.create();
1017:
1018: try
1019: {
1020: overlay.paint(g2Copy, this);
1021: }
1022: catch (Throwable t)
1023: {
1024: log.warn("Exception in Overlay: " + t);
1025: log.warn("paint()", t);
1026: }
1027:
1028: g2Copy.dispose();
1029: }
1030: }
1031: }
1032:
1033: catch (Throwable t)
1034: {
1035: log.warn("paint()", t);
1036: }
1037:
1038: finally
1039: {
1040:• if (g2 != null)
1041: {
1042: g2.dispose();
1043: }
1044: }
1045:
1046: paintComponents(g);
1047: }
1048:
1049: /*******************************************************************************************************************
1050: *
1051: * Flush all image caches.
1052: *
1053: ******************************************************************************************************************/
1054: public void flushAllCaches()
1055: {
1056: log.info("flushAllCaches()");
1057: log.info(">>>> all caches will be recomputed from: " + image);
1058: flushScaledImageCache();
1059:
1060:• if (image != null)
1061: {
1062:• optimizedImage = optimizedImageEnabled ? image.execute(new OptimizeOp()) : image;
1063: }
1064:
1065: else
1066: {
1067: optimizedImage = null;
1068: }
1069: }
1070:
1071: /*******************************************************************************************************************
1072: *
1073: * Flush the cached scaled image.
1074: *
1075: ******************************************************************************************************************/
1076: public void flushScaledImageCache()
1077: {
1078: log.info("flushScaledImageCache()");
1079: scaledImage = null;
1080: }
1081:
1082: /*******************************************************************************************************************
1083: *
1084: *
1085: *
1086: ******************************************************************************************************************/
1087: public void moveOrigin (final int deltaX, final int deltaY)
1088: {
1089: log.info("moveOrigin(" + deltaX + "," + deltaY + ")");
1090: final var position = getOrigin();
1091: position.setLocation(position.getX() + deltaX, position.getY() + deltaY);
1092: setOrigin(position);
1093: }
1094:
1095: /*******************************************************************************************************************
1096: *
1097: * Sets the explicit scale for displaying the current image. This disables the
1098: * fit-to-display-size feature.
1099: *
1100: * @param scale the new scale
1101: *
1102: ******************************************************************************************************************/
1103: public void setScale (final double scale)
1104: {
1105: log.info("setScale(" + scale + ")");
1106: setScale(scale, null);
1107: }
1108:
1109: /*******************************************************************************************************************
1110: *
1111: * Sets the explicit scale for displaying the current image. This disables the
1112: * fit-to-display-size feature. A pivot point is specified: the contents under
1113: * the pivot point don't move during the zoom.
1114: *
1115: * @param scale the new scale
1116: * @param pivotPoint the pivot point (if null, the center of the renderer
1117: * is used)
1118: *
1119: ******************************************************************************************************************/
1120: public void setScale (double scale, final Point pivotPoint)
1121: {
1122: log.info("setScale(" + scale + ", " + pivotPoint + ")");
1123: scale = Math.min(Math.max(scale, minScale), maxScale);
1124:
1125: // if ((scale < MIN_SCALE) || (scale > MAX_SCALE))
1126: // {
1127: // throw new IllegalArgumentException("scale: " + scale);
1128: // }
1129:
1130: final var repaintEnabledSave = repaintEnabled;
1131: repaintEnabled = false;
1132: setFitToDisplaySize(false);
1133: internalSetScale(scale);
1134:
1135:• if (pivotPoint != null)
1136: {
1137: final var imagePivotPoint = getPositionOverImage(pivotPoint);
1138:
1139:• if (imagePivotPoint != null)
1140: {
1141: final var newOrigin = computeOrigin(imagePivotPoint, pivotPoint, scale);
1142:
1143:• if (newOrigin != null)
1144: {
1145: setOrigin(newOrigin);
1146: }
1147: }
1148: }
1149:
1150: // internalSetScale(scale);
1151: repaintEnabled = repaintEnabledSave;
1152: }
1153:
1154: /*******************************************************************************************************************
1155: *
1156: *
1157: *
1158: ******************************************************************************************************************/
1159: public void setMaxScale (final double maxScale)
1160: {
1161: this.maxScale = Math.min(MAX_SCALE, maxScale);
1162: }
1163:
1164: /*******************************************************************************************************************
1165: *
1166: *
1167: *
1168: ******************************************************************************************************************/
1169: public double getMaxScale()
1170: {
1171: return maxScale;
1172: }
1173:
1174: /*******************************************************************************************************************
1175: *
1176: *
1177: *
1178: ******************************************************************************************************************/
1179: public void setMinScale (final double minScale)
1180: {
1181: this.minScale = Math.max(MIN_SCALE, minScale);
1182: }
1183:
1184: /*******************************************************************************************************************
1185: *
1186: *
1187: *
1188: ******************************************************************************************************************/
1189: public double getMinScale()
1190: {
1191: return minScale;
1192: }
1193:
1194: /*******************************************************************************************************************
1195: *
1196: *
1197: *
1198: ******************************************************************************************************************/
1199: public double getFitScale()
1200: {
1201: final var hScale = (double)getAvailableWidth() / image.getWidth();
1202: final var vScale = (double)getAvailableHeight() / image.getHeight();
1203:
1204: return Math.min(hScale, vScale);
1205:
1206: // if (this.scale < 0)
1207: // {
1208: // log.info("SCALE < 0: w:" + w + " h:" + h + " iw:" + iw + " ih:" + ih);
1209: // }
1210: }
1211:
1212: /*******************************************************************************************************************
1213: *
1214: * Centers the image on the renderer, keeping the current scale.
1215: *
1216: ******************************************************************************************************************/
1217: public void centerImage()
1218: {
1219: log.info("centerImage()");
1220: setOrigin(computeCenterPoint());
1221: }
1222:
1223: /*******************************************************************************************************************
1224: *
1225: *
1226: *
1227: ******************************************************************************************************************/
1228: public void fitToDisplaySize()
1229: {
1230: log.info("fitToDisplaySize()");
1231:
1232:• if (image != null)
1233: {
1234: final var saveRepaintEnabled = repaintEnabled;
1235: repaintEnabled = false;
1236: internalSetScale(getFitScale());
1237: centerImage();
1238: repaintEnabled = saveRepaintEnabled;
1239: repaint();
1240: }
1241: }
1242:
1243: /*******************************************************************************************************************
1244: *
1245: * Enables or disables the fit-to-display-size feature.
1246: *
1247: ******************************************************************************************************************/
1248: public void setFitToDisplaySize (final boolean fitToDisplaySize)
1249: {
1250: log.info("setFitToDisplaySize(" + fitToDisplaySize + ")");
1251: this.fitToDisplaySize = fitToDisplaySize;
1252:
1253:• if (fitToDisplaySize)
1254: {
1255: fitToDisplaySize();
1256: }
1257: }
1258:
1259: /*******************************************************************************************************************
1260: *
1261: *
1262: *
1263: ******************************************************************************************************************/
1264: public void addImageRendererListener (final EditableImageRendererListener listener)
1265: {
1266: listenerList.add(listener);
1267: }
1268:
1269: /*******************************************************************************************************************
1270: *
1271: *
1272: *
1273: ******************************************************************************************************************/
1274: public void removeImageRendererListener (final EditableImageRendererListener listener)
1275: {
1276: listenerList.remove(listener);
1277: }
1278:
1279: /*******************************************************************************************************************
1280: *
1281: *
1282: *
1283: ******************************************************************************************************************/
1284: private void internalSetScale (final double scale)
1285: {
1286:• if (this.scale != scale)
1287: {
1288: this.scale = scale;
1289: flushScaledImageCache();
1290: repaint();
1291: }
1292:
1293: fireScaleChangedEvent();
1294: }
1295:
1296: /*******************************************************************************************************************
1297: *
1298: *
1299: *
1300: ******************************************************************************************************************/
1301: private Point computeCenterPoint()
1302: {
1303: return new Point(-(int)Math.round(((getAvailableWidth() / scale) - image.getWidth()) / 2),
1304: -(int)Math.round(((getAvailableHeight() / scale) - image.getHeight()) / 2));
1305: }
1306:
1307: /*******************************************************************************************************************
1308: *
1309: *
1310: *
1311: ******************************************************************************************************************/
1312: private void fireScaleChangedEvent()
1313: {
1314: final var event = new EditableImageRendererEvent(this);
1315:
1316:• for (final var listener : new ArrayList<>(listenerList))
1317: {
1318: try
1319: {
1320: listener.scaleChanged(event);
1321: }
1322: catch (Throwable t)
1323: {
1324: log.warn("Exception in listener: " + t);
1325: log.warn("fireScaleChangedEvent()", t);
1326: }
1327: }
1328: }
1329:
1330: /*******************************************************************************************************************
1331: *
1332: *
1333: *
1334: ******************************************************************************************************************/
1335: private void fireAngleChangedEvent()
1336: {
1337: final var event = new EditableImageRendererEvent(this);
1338:
1339:• for (final var listener : new ArrayList<>(listenerList))
1340: {
1341: try
1342: {
1343: listener.angleChanged(event);
1344: }
1345: catch (Throwable t)
1346: {
1347: log.warn("Exception in listener: " + t);
1348: log.warn("fireAngleChangedEvent()", t);
1349: }
1350: }
1351: }
1352:
1353: /*******************************************************************************************************************
1354: *
1355: *
1356: *
1357: ******************************************************************************************************************/
1358: protected void fireEditingToolActivated (final EditingTool editingTool)
1359: {
1360: final var event = new EditableImageRendererEvent(this, editingTool);
1361:
1362:• for (final var listener : new ArrayList<>(listenerList))
1363: {
1364: try
1365: {
1366: listener.toolActivated(event);
1367: }
1368: catch (Throwable t)
1369: {
1370: log.warn("Exception in listener: " + t);
1371: log.warn("fireEditingToolActivated()", t);
1372: }
1373: }
1374: }
1375:
1376: /*******************************************************************************************************************
1377: *
1378: *
1379: *
1380: ******************************************************************************************************************/
1381: protected void fireEditingToolDeactivated (final EditingTool editingTool)
1382: {
1383: final var event = new EditableImageRendererEvent(this, editingTool);
1384:
1385:• for (final var listener : new ArrayList<>(listenerList))
1386: {
1387: try
1388: {
1389: listener.toolDeactivated(event);
1390: }
1391: catch (Throwable t)
1392: {
1393: log.warn("Exception in listener: " + t);
1394: log.warn("fireEditingToolDeactivated()", t);
1395: }
1396: }
1397: }
1398:
1399: /*******************************************************************************************************************
1400: *
1401: * Updates the scrollbar cursors positions.
1402: *
1403: ******************************************************************************************************************/
1404: private void updateScrollBars()
1405: {
1406:• if (scrollBarsVisible)
1407: {
1408: horizontalScrollBar.setValues(this.origin.x,
1409: (int)Math.round(getAvailableWidth() / scale),
1410: 0,
1411: image.getWidth());
1412: verticalScrollBar.setValues(this.origin.y,
1413: (int)Math.round(getAvailableHeight() / scale),
1414: 0,
1415: image.getHeight());
1416: }
1417: }
1418:
1419: /*******************************************************************************************************************
1420: *
1421: * Lays out the scrollbars in their correct position.
1422: *
1423: ******************************************************************************************************************/
1424: private void layoutScrollBars()
1425: {
1426:• if (scrollBarsVisible && ((previousWidth != getWidth()) || (previohsHeight != getHeight())))
1427: {
1428: horizontalScrollBar.setBounds(0,
1429: getHeight() - scrollbarThickness,
1430: getWidth() - scrollbarThickness,
1431: scrollbarThickness);
1432: verticalScrollBar.setBounds(getWidth() - scrollbarThickness,
1433: 0,
1434: scrollbarThickness,
1435: getHeight() - scrollbarThickness);
1436: filler.setBounds(getWidth() - scrollbarThickness,
1437: getHeight() - scrollbarThickness,
1438: scrollbarThickness,
1439: scrollbarThickness);
1440: previousWidth = getWidth();
1441: previohsHeight = getHeight();
1442: }
1443: }
1444:
1445: /*******************************************************************************************************************
1446: *
1447: * Returns the available width for rendering the image.
1448: *
1449: ******************************************************************************************************************/
1450: private int getAvailableWidth()
1451: {
1452:• return getWidth() - (scrollBarsVisible ? scrollbarThickness : 0);
1453: }
1454:
1455: /*******************************************************************************************************************
1456: *
1457: * Returns the available height for rendering the image.
1458: *
1459: ******************************************************************************************************************/
1460: private int getAvailableHeight()
1461: {
1462:• return getHeight() - (scrollBarsVisible ? scrollbarThickness : 0);
1463: }
1464: }