2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
31 import java
.awt
.image
.BufferedImage
;
33 import jexer
.backend
.ECMA48Terminal
;
34 import jexer
.backend
.MultiScreen
;
35 import jexer
.backend
.SwingTerminal
;
36 import jexer
.bits
.Cell
;
37 import jexer
.event
.TCommandEvent
;
38 import jexer
.event
.TKeypressEvent
;
39 import jexer
.event
.TMouseEvent
;
40 import jexer
.event
.TResizeEvent
;
41 import static jexer
.TCommand
.*;
42 import static jexer
.TKeypress
.*;
45 * TImage renders a piece of a bitmap image on screen.
47 public class TImage
extends TWidget
implements EditMenuUser
{
49 // ------------------------------------------------------------------------
50 // Constants --------------------------------------------------------------
51 // ------------------------------------------------------------------------
54 * Selections for fitting the image to the text cells.
63 * Stretch/shrink the image in both directions to fully fill the text
69 * Scale the image, preserving aspect ratio, to fill the text area
70 * width/height (like letterbox). The background color for the
71 * letterboxed area is specified in scaleBackColor.
76 // ------------------------------------------------------------------------
77 // Variables --------------------------------------------------------------
78 // ------------------------------------------------------------------------
81 * Scaling strategy to use.
83 private Scale scale
= Scale
.NONE
;
86 * Scaling strategy to use.
88 private java
.awt
.Color scaleBackColor
= java
.awt
.Color
.BLACK
;
91 * The action to perform when the user clicks on the image.
93 private TAction clickAction
;
96 * The image to display.
98 private BufferedImage image
;
101 * The original image from construction time.
103 private BufferedImage originalImage
;
106 * The current scaling factor for the image.
108 private double scaleFactor
= 1.0;
111 * The current clockwise rotation for the image.
113 private int clockwise
= 0;
116 * If true, this widget was resized and a new scaled image must be
119 private boolean resized
= false;
122 * Left column of the image. 0 is the left-most column.
127 * Top row of the image. 0 is the top-most row.
132 * The cells containing the broken up image pieces.
134 private Cell cells
[][];
137 * The number of rows in cells[].
139 private int cellRows
;
142 * The number of columns in cells[].
144 private int cellColumns
;
147 * Last text width value.
149 private int lastTextWidth
= -1;
152 * Last text height value.
154 private int lastTextHeight
= -1;
156 // ------------------------------------------------------------------------
157 // Constructors -----------------------------------------------------------
158 // ------------------------------------------------------------------------
161 * Public constructor.
163 * @param parent parent widget
164 * @param x column relative to parent
165 * @param y row relative to parent
166 * @param width number of text cells for width of the image
167 * @param height number of text cells for height of the image
168 * @param image the image to display
169 * @param left left column of the image. 0 is the left-most column.
170 * @param top top row of the image. 0 is the top-most row.
172 public TImage(final TWidget parent
, final int x
, final int y
,
173 final int width
, final int height
,
174 final BufferedImage image
, final int left
, final int top
) {
176 this(parent
, x
, y
, width
, height
, image
, left
, top
, null);
180 * Public constructor.
182 * @param parent parent widget
183 * @param x column relative to parent
184 * @param y row relative to parent
185 * @param width number of text cells for width of the image
186 * @param height number of text cells for height of the image
187 * @param image the image to display
188 * @param left left column of the image. 0 is the left-most column.
189 * @param top top row of the image. 0 is the top-most row.
190 * @param clickAction function to call when mouse is pressed
192 public TImage(final TWidget parent
, final int x
, final int y
,
193 final int width
, final int height
,
194 final BufferedImage image
, final int left
, final int top
,
195 final TAction clickAction
) {
197 // Set parent and window
198 super(parent
, x
, y
, width
, height
);
200 setCursorVisible(false);
201 this.originalImage
= image
;
204 this.clickAction
= clickAction
;
209 // ------------------------------------------------------------------------
210 // Event handlers ---------------------------------------------------------
211 // ------------------------------------------------------------------------
214 * Handle mouse press events.
216 * @param mouse mouse button press event
219 public void onMouseDown(final TMouseEvent mouse
) {
220 if (clickAction
!= null) {
221 clickAction
.DO(this);
229 * @param keypress keystroke event
232 public void onKeypress(final TKeypressEvent keypress
) {
233 if (!keypress
.getKey().isFnKey()) {
234 if (keypress
.getKey().getChar() == '+') {
235 // Make the image bigger.
241 if (keypress
.getKey().getChar() == '-') {
242 // Make the image smaller.
249 if (keypress
.equals(kbAltUp
)) {
250 // Make the image bigger.
256 if (keypress
.equals(kbAltDown
)) {
257 // Make the image smaller.
263 if (keypress
.equals(kbAltRight
)) {
271 if (keypress
.equals(kbAltLeft
)) {
272 // Rotate counter-clockwise.
282 if (keypress
.equals(kbShiftLeft
)) {
285 setScaleType(Scale
.SCALE
);
288 setScaleType(Scale
.NONE
);
291 setScaleType(Scale
.STRETCH
);
295 if (keypress
.equals(kbShiftRight
)) {
298 setScaleType(Scale
.STRETCH
);
301 setScaleType(Scale
.SCALE
);
304 setScaleType(Scale
.NONE
);
309 // Pass to parent for the things we don't care about.
310 super.onKeypress(keypress
);
314 * Handle resize events.
316 * @param event resize event
319 public void onResize(final TResizeEvent event
) {
320 // Get my width/height set correctly.
321 super.onResize(event
);
323 if (scale
== Scale
.NONE
) {
331 * Handle posted command events.
333 * @param command command event
336 public void onCommand(final TCommandEvent command
) {
337 if (command
.equals(cmCopy
)) {
338 // Copy image to clipboard.
339 getClipboard().copyImage(image
);
344 // ------------------------------------------------------------------------
345 // TWidget ----------------------------------------------------------------
346 // ------------------------------------------------------------------------
355 // We have already broken the image up, just draw the last set of
357 for (int x
= 0; (x
< getWidth()) && (x
+ left
< cellColumns
); x
++) {
358 if ((left
+ x
) * lastTextWidth
> image
.getWidth()) {
362 for (int y
= 0; (y
< getHeight()) && (y
+ top
< cellRows
); y
++) {
363 if ((top
+ y
) * lastTextHeight
> image
.getHeight()) {
366 assert (x
+ left
< cellColumns
);
367 assert (y
+ top
< cellRows
);
369 getWindow().putCharXY(x
, y
, cells
[x
+ left
][y
+ top
]);
375 // ------------------------------------------------------------------------
376 // TImage -----------------------------------------------------------------
377 // ------------------------------------------------------------------------
380 * Size cells[][] according to the screen font size.
382 * @param always if true, always resize the cells
384 private void sizeToImage(final boolean always
) {
385 int textWidth
= getScreen().getTextWidth();
386 int textHeight
= getScreen().getTextHeight();
389 image
= rotateImage(originalImage
, clockwise
);
390 image
= scaleImage(image
, scaleFactor
, getWidth(), getHeight(),
391 textWidth
, textHeight
);
394 if ((always
== true) ||
397 && (textWidth
!= lastTextWidth
)
399 && (textHeight
!= lastTextHeight
))
403 cellColumns
= image
.getWidth() / textWidth
;
404 if (cellColumns
* textWidth
< image
.getWidth()) {
407 cellRows
= image
.getHeight() / textHeight
;
408 if (cellRows
* textHeight
< image
.getHeight()) {
412 // Break the image up into an array of cells.
413 cells
= new Cell
[cellColumns
][cellRows
];
415 for (int x
= 0; x
< cellColumns
; x
++) {
416 for (int y
= 0; y
< cellRows
; y
++) {
418 int width
= textWidth
;
419 if ((x
+ 1) * textWidth
> image
.getWidth()) {
420 width
= image
.getWidth() - (x
* textWidth
);
422 int height
= textHeight
;
423 if ((y
+ 1) * textHeight
> image
.getHeight()) {
424 height
= image
.getHeight() - (y
* textHeight
);
427 Cell cell
= new Cell();
428 if ((width
!= textWidth
) || (height
!= textHeight
)) {
429 BufferedImage newImage
;
430 newImage
= new BufferedImage(textWidth
, textHeight
,
431 BufferedImage
.TYPE_INT_ARGB
);
433 java
.awt
.Graphics gr
= newImage
.getGraphics();
434 gr
.drawImage(image
.getSubimage(x
* textWidth
,
435 y
* textHeight
, width
, height
),
438 cell
.setImage(newImage
);
440 cell
.setImage(image
.getSubimage(x
* textWidth
,
441 y
* textHeight
, width
, height
));
448 lastTextWidth
= textWidth
;
449 lastTextHeight
= textHeight
;
452 if ((left
+ getWidth()) > cellColumns
) {
453 left
= cellColumns
- getWidth();
458 if ((top
+ getHeight()) > cellRows
) {
459 top
= cellRows
- getHeight();
467 * Get the top corner to render.
469 * @return the top row
471 public int getTop() {
476 * Set the top corner to render.
478 * @param top the new top row
480 public void setTop(final int top
) {
482 if (this.top
> cellRows
- getHeight()) {
483 this.top
= cellRows
- getHeight();
491 * Get the left corner to render.
493 * @return the left column
495 public int getLeft() {
500 * Set the left corner to render.
502 * @param left the new left column
504 public void setLeft(final int left
) {
506 if (this.left
> cellColumns
- getWidth()) {
507 this.left
= cellColumns
- getWidth();
515 * Get the number of text cell rows for this image.
517 * @return the number of rows
519 public int getRows() {
524 * Get the number of text cell columns for this image.
526 * @return the number of columns
528 public int getColumns() {
533 * Get the raw (unprocessed) image.
537 public BufferedImage
getImage() {
538 return originalImage
;
542 * Set the raw image, and reprocess to make the visible image.
544 * @param image the new image
546 public void setImage(final BufferedImage image
) {
547 this.originalImage
= image
;
553 * Get the visible (processed) image.
555 * @return the image that is currently on screen
557 public BufferedImage
getVisibleImage() {
562 * Get the scaling strategy.
564 * @return Scale.NONE, Scale.STRETCH, etc.
566 public Scale
getScaleType() {
571 * Set the scaling strategy.
573 * @param scale Scale.NONE, Scale.STRETCH, etc.
575 public void setScaleType(final Scale scale
) {
582 * Get the scale factor.
584 * @return the scale factor
586 public double getScaleFactor() {
591 * Set the scale factor. 1.0 means no scaling.
593 * @param scaleFactor the new scale factor
595 public void setScaleFactor(final double scaleFactor
) {
596 this.scaleFactor
= scaleFactor
;
602 * Get the rotation, as degrees.
604 * @return the rotation in degrees
606 public int getRotation() {
617 // Don't know how this happened, but fix it.
626 * Set the rotation, as degrees clockwise.
628 * @param rotation 0, 90, 180, or 270
630 public void setRotation(final int rotation
) {
645 // Don't know how this happened, but fix it.
655 * Scale an image by to be scaleFactor size.
657 * @param image the image to scale
658 * @param factor the scale to make the new image
659 * @param width the number of text cell columns for the destination image
660 * @param height the number of text cell rows for the destination image
661 * @param textWidth the width in pixels for one text cell
662 * @param textHeight the height in pixels for one text cell
664 private BufferedImage
scaleImage(final BufferedImage image
,
665 final double factor
, final int width
, final int height
,
666 final int textWidth
, final int textHeight
) {
668 if ((scale
== Scale
.NONE
) && (Math
.abs(factor
- 1.0) < 0.03)) {
669 // If we are within 3% of 1.0, just return the original image.
678 BufferedImage newImage
= null;
682 destWidth
= (int) (image
.getWidth() * factor
);
683 destHeight
= (int) (image
.getHeight() * factor
);
684 newImage
= new BufferedImage(destWidth
, destHeight
,
685 BufferedImage
.TYPE_INT_ARGB
);
688 destWidth
= width
* textWidth
;
689 destHeight
= height
* textHeight
;
690 newImage
= new BufferedImage(destWidth
, destHeight
,
691 BufferedImage
.TYPE_INT_ARGB
);
694 double a
= (double) image
.getWidth() / image
.getHeight();
695 double b
= (double) (width
* textWidth
) / (height
* textHeight
);
700 System.err.println("Scale: original " + image.getWidth() +
701 "x" + image.getHeight());
702 System.err.println(" screen " + (width * textWidth) +
703 "x" + (height * textHeight));
704 System.err.println("A " + a + " B " + b);
708 // Horizontal letterbox
709 destWidth
= width
* textWidth
;
710 destHeight
= (int) (destWidth
/ a
);
711 y
= ((height
* textHeight
) - destHeight
) / 2;
714 System.err.println("Horizontal letterbox: " + destWidth +
715 "x" + destHeight + ", Y offset " + y);
718 // Vertical letterbox
719 destHeight
= height
* textHeight
;
720 destWidth
= (int) (destHeight
* a
);
721 x
= ((width
* textWidth
) - destWidth
) / 2;
724 System.err.println("Vertical letterbox: " + destWidth +
725 "x" + destHeight + ", X offset " + x);
728 newImage
= new BufferedImage(width
* textWidth
, height
* textHeight
,
729 BufferedImage
.TYPE_INT_ARGB
);
733 java
.awt
.Graphics gr
= newImage
.createGraphics();
734 if (scale
== Scale
.SCALE
) {
735 gr
.setColor(scaleBackColor
);
736 gr
.fillRect(0, 0, width
* textWidth
, height
* textHeight
);
738 gr
.drawImage(image
, x
, y
, destWidth
, destHeight
, null);
744 * Rotate an image either clockwise or counterclockwise.
746 * @param image the image to scale
747 * @param clockwise number of turns clockwise
749 private BufferedImage
rotateImage(final BufferedImage image
,
750 final int clockwise
) {
752 if (clockwise
% 4 == 0) {
756 BufferedImage newImage
= null;
758 if (clockwise
% 4 == 1) {
759 // 90 degrees clockwise
760 newImage
= new BufferedImage(image
.getHeight(), image
.getWidth(),
761 BufferedImage
.TYPE_INT_ARGB
);
762 for (int x
= 0; x
< image
.getWidth(); x
++) {
763 for (int y
= 0; y
< image
.getHeight(); y
++) {
764 newImage
.setRGB(y
, x
,
765 image
.getRGB(x
, image
.getHeight() - 1 - y
));
768 } else if (clockwise
% 4 == 2) {
769 // 180 degrees clockwise
770 newImage
= new BufferedImage(image
.getWidth(), image
.getHeight(),
771 BufferedImage
.TYPE_INT_ARGB
);
772 for (int x
= 0; x
< image
.getWidth(); x
++) {
773 for (int y
= 0; y
< image
.getHeight(); y
++) {
774 newImage
.setRGB(x
, y
,
775 image
.getRGB(image
.getWidth() - 1 - x
,
776 image
.getHeight() - 1 - y
));
779 } else if (clockwise
% 4 == 3) {
780 // 270 degrees clockwise
781 newImage
= new BufferedImage(image
.getHeight(), image
.getWidth(),
782 BufferedImage
.TYPE_INT_ARGB
);
783 for (int x
= 0; x
< image
.getWidth(); x
++) {
784 for (int y
= 0; y
< image
.getHeight(); y
++) {
785 newImage
.setRGB(y
, x
,
786 image
.getRGB(image
.getWidth() - 1 - x
, y
));
794 // ------------------------------------------------------------------------
795 // EditMenuUser -----------------------------------------------------------
796 // ------------------------------------------------------------------------
799 * Check if the cut menu item should be enabled.
801 * @return true if the cut menu item should be enabled
803 public boolean isEditMenuCut() {
808 * Check if the copy menu item should be enabled.
810 * @return true if the copy menu item should be enabled
812 public boolean isEditMenuCopy() {
817 * Check if the paste menu item should be enabled.
819 * @return true if the paste menu item should be enabled
821 public boolean isEditMenuPaste() {
826 * Check if the clear menu item should be enabled.
828 * @return true if the clear menu item should be enabled
830 public boolean isEditMenuClear() {