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