update terminals list
[fanfix.git] / src / jexer / TImage.java
CommitLineData
a69ed767
KL
1/*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 Kevin Lamonte
7 *
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:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
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.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29package jexer;
30
31import java.awt.image.BufferedImage;
32
33import jexer.backend.ECMA48Terminal;
34import jexer.backend.MultiScreen;
35import jexer.backend.SwingTerminal;
36import jexer.bits.Cell;
37import jexer.event.TKeypressEvent;
38import jexer.event.TMouseEvent;
b2441422 39import jexer.event.TResizeEvent;
a69ed767
KL
40import static jexer.TKeypress.*;
41
42/**
43 * TImage renders a piece of a bitmap image on screen.
44 */
45public class TImage extends TWidget {
46
b2441422
KL
47 // ------------------------------------------------------------------------
48 // Constants --------------------------------------------------------------
49 // ------------------------------------------------------------------------
50
51 /**
52 * Selections for fitting the image to the text cells.
53 */
54 public enum Scale {
55 /**
56 * No scaling.
57 */
58 NONE,
59
60 /**
61 * Stretch/shrink the image in both directions to fully fill the text
62 * area width/height.
63 */
64 STRETCH,
65
66 /**
67 * Scale the image, preserving aspect ratio, to fill the text area
68 * width/height (like letterbox). The background color for the
69 * letterboxed area is specified in scaleBackColor.
70 */
71 SCALE,
72 }
73
a69ed767
KL
74 // ------------------------------------------------------------------------
75 // Variables --------------------------------------------------------------
76 // ------------------------------------------------------------------------
77
b2441422
KL
78 /**
79 * Scaling strategy to use.
80 */
81 private Scale scale = Scale.NONE;
82
83 /**
84 * Scaling strategy to use.
85 */
86 private java.awt.Color scaleBackColor = java.awt.Color.BLACK;
87
a69ed767
KL
88 /**
89 * The action to perform when the user clicks on the image.
90 */
91 private TAction clickAction;
92
93 /**
94 * The image to display.
95 */
96 private BufferedImage image;
97
98 /**
99 * The original image from construction time.
100 */
101 private BufferedImage originalImage;
102
103 /**
104 * The current scaling factor for the image.
105 */
106 private double scaleFactor = 1.0;
107
108 /**
109 * The current clockwise rotation for the image.
110 */
111 private int clockwise = 0;
112
b2441422
KL
113 /**
114 * If true, this widget was resized and a new scaled image must be
115 * produced.
116 */
117 private boolean resized = false;
118
a69ed767
KL
119 /**
120 * Left column of the image. 0 is the left-most column.
121 */
122 private int left;
123
124 /**
125 * Top row of the image. 0 is the top-most row.
126 */
127 private int top;
128
129 /**
130 * The cells containing the broken up image pieces.
131 */
132 private Cell cells[][];
133
134 /**
135 * The number of rows in cells[].
136 */
137 private int cellRows;
138
139 /**
140 * The number of columns in cells[].
141 */
142 private int cellColumns;
143
144 /**
145 * Last text width value.
146 */
147 private int lastTextWidth = -1;
148
149 /**
150 * Last text height value.
151 */
152 private int lastTextHeight = -1;
153
154 // ------------------------------------------------------------------------
155 // Constructors -----------------------------------------------------------
156 // ------------------------------------------------------------------------
157
158 /**
159 * Public constructor.
160 *
161 * @param parent parent widget
162 * @param x column relative to parent
163 * @param y row relative to parent
164 * @param width number of text cells for width of the image
165 * @param height number of text cells for height of the image
166 * @param image the image to display
167 * @param left left column of the image. 0 is the left-most column.
382bc294
KL
168 * @param top top row of the image. 0 is the top-most row.
169 */
170 public TImage(final TWidget parent, final int x, final int y,
171 final int width, final int height,
172 final BufferedImage image, final int left, final int top) {
173
174 this(parent, x, y, width, height, image, left, top, null);
175 }
176
177 /**
178 * Public constructor.
179 *
180 * @param parent parent widget
181 * @param x column relative to parent
182 * @param y row relative to parent
183 * @param width number of text cells for width of the image
184 * @param height number of text cells for height of the image
185 * @param image the image to display
186 * @param left left column of the image. 0 is the left-most column.
a69ed767
KL
187 * @param top top row of the image. 0 is the top-most row.
188 * @param clickAction function to call when mouse is pressed
189 */
190 public TImage(final TWidget parent, final int x, final int y,
191 final int width, final int height,
192 final BufferedImage image, final int left, final int top,
193 final TAction clickAction) {
194
195 // Set parent and window
196 super(parent, x, y, width, height);
197
198 setCursorVisible(false);
199 this.originalImage = image;
200 this.left = left;
201 this.top = top;
202 this.clickAction = clickAction;
203
204 sizeToImage(true);
a69ed767
KL
205 }
206
207 // ------------------------------------------------------------------------
208 // Event handlers ---------------------------------------------------------
209 // ------------------------------------------------------------------------
210
a69ed767
KL
211 /**
212 * Handle mouse press events.
213 *
214 * @param mouse mouse button press event
215 */
216 @Override
217 public void onMouseDown(final TMouseEvent mouse) {
218 if (clickAction != null) {
219 clickAction.DO();
220 return;
221 }
222 }
223
224 /**
225 * Handle keystrokes.
226 *
227 * @param keypress keystroke event
228 */
229 @Override
230 public void onKeypress(final TKeypressEvent keypress) {
231 if (!keypress.getKey().isFnKey()) {
232 if (keypress.getKey().getChar() == '+') {
233 // Make the image bigger.
234 scaleFactor *= 1.25;
235 image = null;
236 sizeToImage(true);
237 return;
238 }
239 if (keypress.getKey().getChar() == '-') {
240 // Make the image smaller.
241 scaleFactor *= 0.80;
242 image = null;
243 sizeToImage(true);
244 return;
245 }
246 }
247 if (keypress.equals(kbAltUp)) {
248 // Make the image bigger.
249 scaleFactor *= 1.25;
250 image = null;
251 sizeToImage(true);
252 return;
253 }
254 if (keypress.equals(kbAltDown)) {
255 // Make the image smaller.
256 scaleFactor *= 0.80;
257 image = null;
258 sizeToImage(true);
259 return;
260 }
261 if (keypress.equals(kbAltRight)) {
262 // Rotate clockwise.
263 clockwise++;
264 clockwise %= 4;
265 image = null;
266 sizeToImage(true);
267 return;
268 }
269 if (keypress.equals(kbAltLeft)) {
270 // Rotate counter-clockwise.
271 clockwise--;
272 if (clockwise < 0) {
273 clockwise = 3;
274 }
275 image = null;
276 sizeToImage(true);
277 return;
278 }
279
b2441422
KL
280 if (keypress.equals(kbShiftLeft)) {
281 switch (scale) {
282 case NONE:
283 setScaleType(Scale.SCALE);
284 return;
285 case STRETCH:
286 setScaleType(Scale.NONE);
287 return;
288 case SCALE:
289 setScaleType(Scale.STRETCH);
290 return;
291 }
292 }
293 if (keypress.equals(kbShiftRight)) {
294 switch (scale) {
295 case NONE:
296 setScaleType(Scale.STRETCH);
297 return;
298 case STRETCH:
299 setScaleType(Scale.SCALE);
300 return;
301 case SCALE:
302 setScaleType(Scale.NONE);
303 return;
304 }
305 }
306
a69ed767
KL
307 // Pass to parent for the things we don't care about.
308 super.onKeypress(keypress);
309 }
310
b2441422
KL
311 /**
312 * Handle resize events.
313 *
314 * @param event resize event
315 */
316 @Override
317 public void onResize(final TResizeEvent event) {
318 // Get my width/height set correctly.
319 super.onResize(event);
320
321 if (scale == Scale.NONE) {
322 return;
323 }
324 image = null;
325 resized = true;
326 }
327
a69ed767
KL
328 // ------------------------------------------------------------------------
329 // TWidget ----------------------------------------------------------------
330 // ------------------------------------------------------------------------
331
332 /**
333 * Draw the image.
334 */
335 @Override
336 public void draw() {
337 sizeToImage(false);
338
339 // We have already broken the image up, just draw the last set of
340 // cells.
341 for (int x = 0; (x < getWidth()) && (x + left < cellColumns); x++) {
342 if ((left + x) * lastTextWidth > image.getWidth()) {
343 continue;
344 }
345
346 for (int y = 0; (y < getHeight()) && (y + top < cellRows); y++) {
347 if ((top + y) * lastTextHeight > image.getHeight()) {
348 continue;
349 }
350 assert (x + left < cellColumns);
351 assert (y + top < cellRows);
352
353 getWindow().putCharXY(x, y, cells[x + left][y + top]);
354 }
355 }
356
357 }
358
359 // ------------------------------------------------------------------------
360 // TImage -----------------------------------------------------------------
361 // ------------------------------------------------------------------------
362
363 /**
364 * Size cells[][] according to the screen font size.
365 *
366 * @param always if true, always resize the cells
367 */
368 private void sizeToImage(final boolean always) {
369 int textWidth = 16;
370 int textHeight = 20;
371
372 if (getScreen() instanceof SwingTerminal) {
373 SwingTerminal terminal = (SwingTerminal) getScreen();
374
375 textWidth = terminal.getTextWidth();
376 textHeight = terminal.getTextHeight();
377 } if (getScreen() instanceof MultiScreen) {
378 MultiScreen terminal = (MultiScreen) getScreen();
379
380 textWidth = terminal.getTextWidth();
381 textHeight = terminal.getTextHeight();
382 } else if (getScreen() instanceof ECMA48Terminal) {
383 ECMA48Terminal terminal = (ECMA48Terminal) getScreen();
384
385 textWidth = terminal.getTextWidth();
386 textHeight = terminal.getTextHeight();
387 }
388
389 if (image == null) {
b2441422
KL
390 image = rotateImage(originalImage, clockwise);
391 image = scaleImage(image, scaleFactor, getWidth(), getHeight(),
392 textWidth, textHeight);
a69ed767
KL
393 }
394
395 if ((always == true) ||
b2441422 396 (resized == true) ||
a69ed767
KL
397 ((textWidth > 0)
398 && (textWidth != lastTextWidth)
399 && (textHeight > 0)
400 && (textHeight != lastTextHeight))
401 ) {
b2441422
KL
402 resized = false;
403
a69ed767
KL
404 cellColumns = image.getWidth() / textWidth;
405 if (cellColumns * textWidth < image.getWidth()) {
406 cellColumns++;
407 }
408 cellRows = image.getHeight() / textHeight;
409 if (cellRows * textHeight < image.getHeight()) {
410 cellRows++;
411 }
412
413 // Break the image up into an array of cells.
414 cells = new Cell[cellColumns][cellRows];
415
416 for (int x = 0; x < cellColumns; x++) {
417 for (int y = 0; y < cellRows; y++) {
418
419 int width = textWidth;
420 if ((x + 1) * textWidth > image.getWidth()) {
421 width = image.getWidth() - (x * textWidth);
422 }
423 int height = textHeight;
424 if ((y + 1) * textHeight > image.getHeight()) {
425 height = image.getHeight() - (y * textHeight);
426 }
427
428 Cell cell = new Cell();
429 cell.setImage(image.getSubimage(x * textWidth,
430 y * textHeight, width, height));
431
432 cells[x][y] = cell;
433 }
434 }
435
436 lastTextWidth = textWidth;
437 lastTextHeight = textHeight;
438 }
439
440 if ((left + getWidth()) > cellColumns) {
441 left = cellColumns - getWidth();
442 }
443 if (left < 0) {
444 left = 0;
445 }
446 if ((top + getHeight()) > cellRows) {
447 top = cellRows - getHeight();
448 }
449 if (top < 0) {
450 top = 0;
451 }
452 }
453
454 /**
455 * Get the top corner to render.
456 *
457 * @return the top row
458 */
459 public int getTop() {
460 return top;
461 }
462
463 /**
464 * Set the top corner to render.
465 *
466 * @param top the new top row
467 */
468 public void setTop(final int top) {
469 this.top = top;
470 if (this.top > cellRows - getHeight()) {
471 this.top = cellRows - getHeight();
472 }
473 if (this.top < 0) {
474 this.top = 0;
475 }
476 }
477
478 /**
479 * Get the left corner to render.
480 *
481 * @return the left column
482 */
483 public int getLeft() {
484 return left;
485 }
486
487 /**
488 * Set the left corner to render.
489 *
490 * @param left the new left column
491 */
492 public void setLeft(final int left) {
493 this.left = left;
494 if (this.left > cellColumns - getWidth()) {
495 this.left = cellColumns - getWidth();
496 }
497 if (this.left < 0) {
498 this.left = 0;
499 }
500 }
501
502 /**
503 * Get the number of text cell rows for this image.
504 *
505 * @return the number of rows
506 */
507 public int getRows() {
508 return cellRows;
509 }
510
511 /**
512 * Get the number of text cell columns for this image.
513 *
514 * @return the number of columns
515 */
516 public int getColumns() {
517 return cellColumns;
518 }
519
b2441422
KL
520 /**
521 * Get the raw (unprocessed) image.
522 *
523 * @return the image
524 */
525 public BufferedImage getImage() {
526 return originalImage;
527 }
528
529 /**
530 * Set the raw image, and reprocess to make the visible image.
531 *
532 * @param image the new image
533 */
534 public void setImage(final BufferedImage image) {
535 this.originalImage = image;
536 this.image = null;
537 sizeToImage(true);
538 }
539
540 /**
541 * Get the visible (processed) image.
542 *
543 * @return the image that is currently on screen
544 */
545 public BufferedImage getVisibleImage() {
546 return image;
547 }
548
549 /**
550 * Get the scaling strategy.
551 *
552 * @return Scale.NONE, Scale.STRETCH, etc.
553 */
554 public Scale getScaleType() {
555 return scale;
556 }
557
558 /**
559 * Set the scaling strategy.
560 *
561 * @param scale Scale.NONE, Scale.STRETCH, etc.
562 */
563 public void setScaleType(final Scale scale) {
564 this.scale = scale;
565 this.image = null;
566 sizeToImage(true);
567 }
568
569 /**
570 * Get the scale factor.
571 *
572 * @return the scale factor
573 */
574 public double getScaleFactor() {
575 return scaleFactor;
576 }
577
578 /**
579 * Set the scale factor. 1.0 means no scaling.
580 *
581 * @param scaleFactor the new scale factor
582 */
583 public void setScaleFactor(final double scaleFactor) {
584 this.scaleFactor = scaleFactor;
585 image = null;
586 sizeToImage(true);
587 }
588
589 /**
590 * Get the rotation, as degrees.
591 *
592 * @return the rotation in degrees
593 */
594 public int getRotation() {
595 switch (clockwise) {
596 case 0:
597 return 0;
598 case 1:
599 return 90;
600 case 2:
601 return 180;
602 case 3:
603 return 270;
604 default:
605 // Don't know how this happened, but fix it.
606 clockwise = 0;
607 image = null;
608 sizeToImage(true);
609 return 0;
610 }
611 }
612
613 /**
614 * Set the rotation, as degrees clockwise.
615 *
616 * @param rotation 0, 90, 180, or 270
617 */
618 public void setRotation(final int rotation) {
619 switch (rotation) {
620 case 0:
621 clockwise = 0;
622 break;
623 case 90:
624 clockwise = 1;
625 break;
626 case 180:
627 clockwise = 2;
628 break;
629 case 270:
630 clockwise = 3;
631 break;
632 default:
633 // Don't know how this happened, but fix it.
634 clockwise = 0;
635 break;
636 }
637
638 image = null;
639 sizeToImage(true);
640 }
641
a69ed767
KL
642 /**
643 * Scale an image by to be scaleFactor size.
644 *
645 * @param image the image to scale
646 * @param factor the scale to make the new image
b2441422
KL
647 * @param width the number of text cell columns for the destination image
648 * @param height the number of text cell rows for the destination image
649 * @param textWidth the width in pixels for one text cell
650 * @param textHeight the height in pixels for one text cell
a69ed767
KL
651 */
652 private BufferedImage scaleImage(final BufferedImage image,
b2441422
KL
653 final double factor, final int width, final int height,
654 final int textWidth, final int textHeight) {
a69ed767 655
b2441422 656 if ((scale == Scale.NONE) && (Math.abs(factor - 1.0) < 0.03)) {
a69ed767
KL
657 // If we are within 3% of 1.0, just return the original image.
658 return image;
659 }
660
b2441422
KL
661 int destWidth = 0;
662 int destHeight = 0;
663 int x = 0;
664 int y = 0;
a69ed767 665
b2441422
KL
666 BufferedImage newImage = null;
667
668 switch (scale) {
669 case NONE:
670 destWidth = (int) (image.getWidth() * factor);
671 destHeight = (int) (image.getHeight() * factor);
672 newImage = new BufferedImage(destWidth, destHeight,
673 BufferedImage.TYPE_INT_ARGB);
674 break;
675 case STRETCH:
676 destWidth = width * textWidth;
677 destHeight = height * textHeight;
678 newImage = new BufferedImage(destWidth, destHeight,
679 BufferedImage.TYPE_INT_ARGB);
680 break;
681 case SCALE:
682 double a = (double) image.getWidth() / image.getHeight();
683 double b = (double) (width * textWidth) / (height * textHeight);
684 assert (a > 0);
685 assert (b > 0);
686
687 /*
688 System.err.println("Scale: original " + image.getWidth() +
689 "x" + image.getHeight());
690 System.err.println(" screen " + (width * textWidth) +
691 "x" + (height * textHeight));
692 System.err.println("A " + a + " B " + b);
693 */
694
695 if (a > b) {
696 // Horizontal letterbox
697 destWidth = width * textWidth;
698 destHeight = (int) (destWidth / a);
699 y = ((height * textHeight) - destHeight) / 2;
700 assert (y >= 0);
701 /*
702 System.err.println("Horizontal letterbox: " + destWidth +
703 "x" + destHeight + ", Y offset " + y);
704 */
705 } else {
706 // Vertical letterbox
707 destHeight = height * textHeight;
708 destWidth = (int) (destHeight * a);
709 x = ((width * textWidth) - destWidth) / 2;
710 assert (x >= 0);
711 /*
712 System.err.println("Vertical letterbox: " + destWidth +
713 "x" + destHeight + ", X offset " + x);
714 */
715 }
716 newImage = new BufferedImage(width * textWidth, height * textHeight,
717 BufferedImage.TYPE_INT_ARGB);
718 break;
719 }
a69ed767
KL
720
721 java.awt.Graphics gr = newImage.createGraphics();
b2441422
KL
722 if (scale == Scale.SCALE) {
723 gr.setColor(scaleBackColor);
724 gr.fillRect(0, 0, width * textWidth, height * textHeight);
725 }
726 gr.drawImage(image, x, y, destWidth, destHeight, null);
a69ed767 727 gr.dispose();
a69ed767
KL
728 return newImage;
729 }
730
731 /**
732 * Rotate an image either clockwise or counterclockwise.
733 *
734 * @param image the image to scale
735 * @param clockwise number of turns clockwise
736 */
737 private BufferedImage rotateImage(final BufferedImage image,
738 final int clockwise) {
739
740 if (clockwise % 4 == 0) {
741 return image;
742 }
743
744 BufferedImage newImage = null;
745
746 if (clockwise % 4 == 1) {
747 // 90 degrees clockwise
748 newImage = new BufferedImage(image.getHeight(), image.getWidth(),
749 BufferedImage.TYPE_INT_ARGB);
750 for (int x = 0; x < image.getWidth(); x++) {
751 for (int y = 0; y < image.getHeight(); y++) {
752 newImage.setRGB(y, x,
753 image.getRGB(x, image.getHeight() - 1 - y));
754 }
755 }
756 } else if (clockwise % 4 == 2) {
757 // 180 degrees clockwise
758 newImage = new BufferedImage(image.getWidth(), image.getHeight(),
759 BufferedImage.TYPE_INT_ARGB);
760 for (int x = 0; x < image.getWidth(); x++) {
761 for (int y = 0; y < image.getHeight(); y++) {
762 newImage.setRGB(x, y,
763 image.getRGB(image.getWidth() - 1 - x,
764 image.getHeight() - 1 - y));
765 }
766 }
767 } else if (clockwise % 4 == 3) {
768 // 270 degrees clockwise
769 newImage = new BufferedImage(image.getHeight(), image.getWidth(),
770 BufferedImage.TYPE_INT_ARGB);
771 for (int x = 0; x < image.getWidth(); x++) {
772 for (int y = 0; y < image.getHeight(); y++) {
773 newImage.setRGB(y, x,
774 image.getRGB(image.getWidth() - 1 - x, y));
775 }
776 }
777 }
778
779 return newImage;
780 }
781
782}