#65 stretch images to fit into text cells
[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();
428 cell.setImage(image.getSubimage(x * textWidth,
429 y * textHeight, width, height));
430
431 cells[x][y] = cell;
432 }
433 }
434
435 lastTextWidth = textWidth;
436 lastTextHeight = textHeight;
437 }
438
439 if ((left + getWidth()) > cellColumns) {
440 left = cellColumns - getWidth();
441 }
442 if (left < 0) {
443 left = 0;
444 }
445 if ((top + getHeight()) > cellRows) {
446 top = cellRows - getHeight();
447 }
448 if (top < 0) {
449 top = 0;
450 }
451 }
452
453 /**
454 * Get the top corner to render.
455 *
456 * @return the top row
457 */
458 public int getTop() {
459 return top;
460 }
461
462 /**
463 * Set the top corner to render.
464 *
465 * @param top the new top row
466 */
467 public void setTop(final int top) {
468 this.top = top;
469 if (this.top > cellRows - getHeight()) {
470 this.top = cellRows - getHeight();
471 }
472 if (this.top < 0) {
473 this.top = 0;
474 }
475 }
476
477 /**
478 * Get the left corner to render.
479 *
480 * @return the left column
481 */
482 public int getLeft() {
483 return left;
484 }
485
486 /**
487 * Set the left corner to render.
488 *
489 * @param left the new left column
490 */
491 public void setLeft(final int left) {
492 this.left = left;
493 if (this.left > cellColumns - getWidth()) {
494 this.left = cellColumns - getWidth();
495 }
496 if (this.left < 0) {
497 this.left = 0;
498 }
499 }
500
501 /**
502 * Get the number of text cell rows for this image.
503 *
504 * @return the number of rows
505 */
506 public int getRows() {
507 return cellRows;
508 }
509
510 /**
511 * Get the number of text cell columns for this image.
512 *
513 * @return the number of columns
514 */
515 public int getColumns() {
516 return cellColumns;
517 }
518
b2441422
KL
519 /**
520 * Get the raw (unprocessed) image.
521 *
522 * @return the image
523 */
524 public BufferedImage getImage() {
525 return originalImage;
526 }
527
528 /**
529 * Set the raw image, and reprocess to make the visible image.
530 *
531 * @param image the new image
532 */
533 public void setImage(final BufferedImage image) {
534 this.originalImage = image;
535 this.image = null;
536 sizeToImage(true);
537 }
538
539 /**
540 * Get the visible (processed) image.
541 *
542 * @return the image that is currently on screen
543 */
544 public BufferedImage getVisibleImage() {
545 return image;
546 }
547
548 /**
549 * Get the scaling strategy.
550 *
551 * @return Scale.NONE, Scale.STRETCH, etc.
552 */
553 public Scale getScaleType() {
554 return scale;
555 }
556
557 /**
558 * Set the scaling strategy.
559 *
560 * @param scale Scale.NONE, Scale.STRETCH, etc.
561 */
562 public void setScaleType(final Scale scale) {
563 this.scale = scale;
564 this.image = null;
565 sizeToImage(true);
566 }
567
568 /**
569 * Get the scale factor.
570 *
571 * @return the scale factor
572 */
573 public double getScaleFactor() {
574 return scaleFactor;
575 }
576
577 /**
578 * Set the scale factor. 1.0 means no scaling.
579 *
580 * @param scaleFactor the new scale factor
581 */
582 public void setScaleFactor(final double scaleFactor) {
583 this.scaleFactor = scaleFactor;
584 image = null;
585 sizeToImage(true);
586 }
587
588 /**
589 * Get the rotation, as degrees.
590 *
591 * @return the rotation in degrees
592 */
593 public int getRotation() {
594 switch (clockwise) {
595 case 0:
596 return 0;
597 case 1:
598 return 90;
599 case 2:
600 return 180;
601 case 3:
602 return 270;
603 default:
604 // Don't know how this happened, but fix it.
605 clockwise = 0;
606 image = null;
607 sizeToImage(true);
608 return 0;
609 }
610 }
611
612 /**
613 * Set the rotation, as degrees clockwise.
614 *
615 * @param rotation 0, 90, 180, or 270
616 */
617 public void setRotation(final int rotation) {
618 switch (rotation) {
619 case 0:
620 clockwise = 0;
621 break;
622 case 90:
623 clockwise = 1;
624 break;
625 case 180:
626 clockwise = 2;
627 break;
628 case 270:
629 clockwise = 3;
630 break;
631 default:
632 // Don't know how this happened, but fix it.
633 clockwise = 0;
634 break;
635 }
636
637 image = null;
638 sizeToImage(true);
639 }
640
a69ed767
KL
641 /**
642 * Scale an image by to be scaleFactor size.
643 *
644 * @param image the image to scale
645 * @param factor the scale to make the new image
b2441422
KL
646 * @param width the number of text cell columns for the destination image
647 * @param height the number of text cell rows for the destination image
648 * @param textWidth the width in pixels for one text cell
649 * @param textHeight the height in pixels for one text cell
a69ed767
KL
650 */
651 private BufferedImage scaleImage(final BufferedImage image,
b2441422
KL
652 final double factor, final int width, final int height,
653 final int textWidth, final int textHeight) {
a69ed767 654
b2441422 655 if ((scale == Scale.NONE) && (Math.abs(factor - 1.0) < 0.03)) {
a69ed767
KL
656 // If we are within 3% of 1.0, just return the original image.
657 return image;
658 }
659
b2441422
KL
660 int destWidth = 0;
661 int destHeight = 0;
662 int x = 0;
663 int y = 0;
a69ed767 664
b2441422
KL
665 BufferedImage newImage = null;
666
667 switch (scale) {
668 case NONE:
669 destWidth = (int) (image.getWidth() * factor);
670 destHeight = (int) (image.getHeight() * factor);
671 newImage = new BufferedImage(destWidth, destHeight,
672 BufferedImage.TYPE_INT_ARGB);
673 break;
674 case STRETCH:
675 destWidth = width * textWidth;
676 destHeight = height * textHeight;
677 newImage = new BufferedImage(destWidth, destHeight,
678 BufferedImage.TYPE_INT_ARGB);
679 break;
680 case SCALE:
681 double a = (double) image.getWidth() / image.getHeight();
682 double b = (double) (width * textWidth) / (height * textHeight);
683 assert (a > 0);
684 assert (b > 0);
685
686 /*
687 System.err.println("Scale: original " + image.getWidth() +
688 "x" + image.getHeight());
689 System.err.println(" screen " + (width * textWidth) +
690 "x" + (height * textHeight));
691 System.err.println("A " + a + " B " + b);
692 */
693
694 if (a > b) {
695 // Horizontal letterbox
696 destWidth = width * textWidth;
697 destHeight = (int) (destWidth / a);
698 y = ((height * textHeight) - destHeight) / 2;
699 assert (y >= 0);
700 /*
701 System.err.println("Horizontal letterbox: " + destWidth +
702 "x" + destHeight + ", Y offset " + y);
703 */
704 } else {
705 // Vertical letterbox
706 destHeight = height * textHeight;
707 destWidth = (int) (destHeight * a);
708 x = ((width * textWidth) - destWidth) / 2;
709 assert (x >= 0);
710 /*
711 System.err.println("Vertical letterbox: " + destWidth +
712 "x" + destHeight + ", X offset " + x);
713 */
714 }
715 newImage = new BufferedImage(width * textWidth, height * textHeight,
716 BufferedImage.TYPE_INT_ARGB);
717 break;
718 }
a69ed767
KL
719
720 java.awt.Graphics gr = newImage.createGraphics();
b2441422
KL
721 if (scale == Scale.SCALE) {
722 gr.setColor(scaleBackColor);
723 gr.fillRect(0, 0, width * textWidth, height * textHeight);
724 }
725 gr.drawImage(image, x, y, destWidth, destHeight, null);
a69ed767 726 gr.dispose();
a69ed767
KL
727 return newImage;
728 }
729
730 /**
731 * Rotate an image either clockwise or counterclockwise.
732 *
733 * @param image the image to scale
734 * @param clockwise number of turns clockwise
735 */
736 private BufferedImage rotateImage(final BufferedImage image,
737 final int clockwise) {
738
739 if (clockwise % 4 == 0) {
740 return image;
741 }
742
743 BufferedImage newImage = null;
744
745 if (clockwise % 4 == 1) {
746 // 90 degrees clockwise
747 newImage = new BufferedImage(image.getHeight(), image.getWidth(),
748 BufferedImage.TYPE_INT_ARGB);
749 for (int x = 0; x < image.getWidth(); x++) {
750 for (int y = 0; y < image.getHeight(); y++) {
751 newImage.setRGB(y, x,
752 image.getRGB(x, image.getHeight() - 1 - y));
753 }
754 }
755 } else if (clockwise % 4 == 2) {
756 // 180 degrees clockwise
757 newImage = new BufferedImage(image.getWidth(), image.getHeight(),
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(x, y,
762 image.getRGB(image.getWidth() - 1 - x,
763 image.getHeight() - 1 - y));
764 }
765 }
766 } else if (clockwise % 4 == 3) {
767 // 270 degrees clockwise
768 newImage = new BufferedImage(image.getHeight(), image.getWidth(),
769 BufferedImage.TYPE_INT_ARGB);
770 for (int x = 0; x < image.getWidth(); x++) {
771 for (int y = 0; y < image.getHeight(); y++) {
772 newImage.setRGB(y, x,
773 image.getRGB(image.getWidth() - 1 - x, y));
774 }
775 }
776 }
777
778 return newImage;
779 }
780
f8b30e4b
KL
781 // ------------------------------------------------------------------------
782 // EditMenuUser -----------------------------------------------------------
783 // ------------------------------------------------------------------------
784
785 /**
786 * Check if the cut menu item should be enabled.
787 *
788 * @return true if the cut menu item should be enabled
789 */
790 public boolean isEditMenuCut() {
791 return false;
792 }
793
794 /**
795 * Check if the copy menu item should be enabled.
796 *
797 * @return true if the copy menu item should be enabled
798 */
799 public boolean isEditMenuCopy() {
800 return true;
801 }
802
803 /**
804 * Check if the paste menu item should be enabled.
805 *
806 * @return true if the paste menu item should be enabled
807 */
808 public boolean isEditMenuPaste() {
809 return false;
810 }
811
812 /**
813 * Check if the clear menu item should be enabled.
814 *
815 * @return true if the clear menu item should be enabled
816 */
817 public boolean isEditMenuClear() {
818 return false;
819 }
820
a69ed767 821}