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