Add 'src/jexer/' from commit 'cf01c92f5809a0732409e280fb0f32f27393618d'
[fanfix.git] / src / jexer / TImage.java
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 */
29 package jexer;
30
31 import java.awt.image.BufferedImage;
32
33 import jexer.backend.ECMA48Terminal;
34 import jexer.backend.MultiScreen;
35 import jexer.backend.SwingTerminal;
36 import jexer.bits.Cell;
37 import jexer.event.TKeypressEvent;
38 import jexer.event.TMouseEvent;
39 import jexer.event.TResizeEvent;
40 import static jexer.TKeypress.*;
41
42 /**
43 * TImage renders a piece of a bitmap image on screen.
44 */
45 public class TImage extends TWidget {
46
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
74 // ------------------------------------------------------------------------
75 // Variables --------------------------------------------------------------
76 // ------------------------------------------------------------------------
77
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
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
113 /**
114 * If true, this widget was resized and a new scaled image must be
115 * produced.
116 */
117 private boolean resized = false;
118
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.
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.
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);
205 }
206
207 // ------------------------------------------------------------------------
208 // Event handlers ---------------------------------------------------------
209 // ------------------------------------------------------------------------
210
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(this);
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
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
307 // Pass to parent for the things we don't care about.
308 super.onKeypress(keypress);
309 }
310
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
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 = getScreen().getTextWidth();
370 int textHeight = getScreen().getTextHeight();
371
372 if (image == null) {
373 image = rotateImage(originalImage, clockwise);
374 image = scaleImage(image, scaleFactor, getWidth(), getHeight(),
375 textWidth, textHeight);
376 }
377
378 if ((always == true) ||
379 (resized == true) ||
380 ((textWidth > 0)
381 && (textWidth != lastTextWidth)
382 && (textHeight > 0)
383 && (textHeight != lastTextHeight))
384 ) {
385 resized = false;
386
387 cellColumns = image.getWidth() / textWidth;
388 if (cellColumns * textWidth < image.getWidth()) {
389 cellColumns++;
390 }
391 cellRows = image.getHeight() / textHeight;
392 if (cellRows * textHeight < image.getHeight()) {
393 cellRows++;
394 }
395
396 // Break the image up into an array of cells.
397 cells = new Cell[cellColumns][cellRows];
398
399 for (int x = 0; x < cellColumns; x++) {
400 for (int y = 0; y < cellRows; y++) {
401
402 int width = textWidth;
403 if ((x + 1) * textWidth > image.getWidth()) {
404 width = image.getWidth() - (x * textWidth);
405 }
406 int height = textHeight;
407 if ((y + 1) * textHeight > image.getHeight()) {
408 height = image.getHeight() - (y * textHeight);
409 }
410
411 Cell cell = new Cell();
412 cell.setImage(image.getSubimage(x * textWidth,
413 y * textHeight, width, height));
414
415 cells[x][y] = cell;
416 }
417 }
418
419 lastTextWidth = textWidth;
420 lastTextHeight = textHeight;
421 }
422
423 if ((left + getWidth()) > cellColumns) {
424 left = cellColumns - getWidth();
425 }
426 if (left < 0) {
427 left = 0;
428 }
429 if ((top + getHeight()) > cellRows) {
430 top = cellRows - getHeight();
431 }
432 if (top < 0) {
433 top = 0;
434 }
435 }
436
437 /**
438 * Get the top corner to render.
439 *
440 * @return the top row
441 */
442 public int getTop() {
443 return top;
444 }
445
446 /**
447 * Set the top corner to render.
448 *
449 * @param top the new top row
450 */
451 public void setTop(final int top) {
452 this.top = top;
453 if (this.top > cellRows - getHeight()) {
454 this.top = cellRows - getHeight();
455 }
456 if (this.top < 0) {
457 this.top = 0;
458 }
459 }
460
461 /**
462 * Get the left corner to render.
463 *
464 * @return the left column
465 */
466 public int getLeft() {
467 return left;
468 }
469
470 /**
471 * Set the left corner to render.
472 *
473 * @param left the new left column
474 */
475 public void setLeft(final int left) {
476 this.left = left;
477 if (this.left > cellColumns - getWidth()) {
478 this.left = cellColumns - getWidth();
479 }
480 if (this.left < 0) {
481 this.left = 0;
482 }
483 }
484
485 /**
486 * Get the number of text cell rows for this image.
487 *
488 * @return the number of rows
489 */
490 public int getRows() {
491 return cellRows;
492 }
493
494 /**
495 * Get the number of text cell columns for this image.
496 *
497 * @return the number of columns
498 */
499 public int getColumns() {
500 return cellColumns;
501 }
502
503 /**
504 * Get the raw (unprocessed) image.
505 *
506 * @return the image
507 */
508 public BufferedImage getImage() {
509 return originalImage;
510 }
511
512 /**
513 * Set the raw image, and reprocess to make the visible image.
514 *
515 * @param image the new image
516 */
517 public void setImage(final BufferedImage image) {
518 this.originalImage = image;
519 this.image = null;
520 sizeToImage(true);
521 }
522
523 /**
524 * Get the visible (processed) image.
525 *
526 * @return the image that is currently on screen
527 */
528 public BufferedImage getVisibleImage() {
529 return image;
530 }
531
532 /**
533 * Get the scaling strategy.
534 *
535 * @return Scale.NONE, Scale.STRETCH, etc.
536 */
537 public Scale getScaleType() {
538 return scale;
539 }
540
541 /**
542 * Set the scaling strategy.
543 *
544 * @param scale Scale.NONE, Scale.STRETCH, etc.
545 */
546 public void setScaleType(final Scale scale) {
547 this.scale = scale;
548 this.image = null;
549 sizeToImage(true);
550 }
551
552 /**
553 * Get the scale factor.
554 *
555 * @return the scale factor
556 */
557 public double getScaleFactor() {
558 return scaleFactor;
559 }
560
561 /**
562 * Set the scale factor. 1.0 means no scaling.
563 *
564 * @param scaleFactor the new scale factor
565 */
566 public void setScaleFactor(final double scaleFactor) {
567 this.scaleFactor = scaleFactor;
568 image = null;
569 sizeToImage(true);
570 }
571
572 /**
573 * Get the rotation, as degrees.
574 *
575 * @return the rotation in degrees
576 */
577 public int getRotation() {
578 switch (clockwise) {
579 case 0:
580 return 0;
581 case 1:
582 return 90;
583 case 2:
584 return 180;
585 case 3:
586 return 270;
587 default:
588 // Don't know how this happened, but fix it.
589 clockwise = 0;
590 image = null;
591 sizeToImage(true);
592 return 0;
593 }
594 }
595
596 /**
597 * Set the rotation, as degrees clockwise.
598 *
599 * @param rotation 0, 90, 180, or 270
600 */
601 public void setRotation(final int rotation) {
602 switch (rotation) {
603 case 0:
604 clockwise = 0;
605 break;
606 case 90:
607 clockwise = 1;
608 break;
609 case 180:
610 clockwise = 2;
611 break;
612 case 270:
613 clockwise = 3;
614 break;
615 default:
616 // Don't know how this happened, but fix it.
617 clockwise = 0;
618 break;
619 }
620
621 image = null;
622 sizeToImage(true);
623 }
624
625 /**
626 * Scale an image by to be scaleFactor size.
627 *
628 * @param image the image to scale
629 * @param factor the scale to make the new image
630 * @param width the number of text cell columns for the destination image
631 * @param height the number of text cell rows for the destination image
632 * @param textWidth the width in pixels for one text cell
633 * @param textHeight the height in pixels for one text cell
634 */
635 private BufferedImage scaleImage(final BufferedImage image,
636 final double factor, final int width, final int height,
637 final int textWidth, final int textHeight) {
638
639 if ((scale == Scale.NONE) && (Math.abs(factor - 1.0) < 0.03)) {
640 // If we are within 3% of 1.0, just return the original image.
641 return image;
642 }
643
644 int destWidth = 0;
645 int destHeight = 0;
646 int x = 0;
647 int y = 0;
648
649 BufferedImage newImage = null;
650
651 switch (scale) {
652 case NONE:
653 destWidth = (int) (image.getWidth() * factor);
654 destHeight = (int) (image.getHeight() * factor);
655 newImage = new BufferedImage(destWidth, destHeight,
656 BufferedImage.TYPE_INT_ARGB);
657 break;
658 case STRETCH:
659 destWidth = width * textWidth;
660 destHeight = height * textHeight;
661 newImage = new BufferedImage(destWidth, destHeight,
662 BufferedImage.TYPE_INT_ARGB);
663 break;
664 case SCALE:
665 double a = (double) image.getWidth() / image.getHeight();
666 double b = (double) (width * textWidth) / (height * textHeight);
667 assert (a > 0);
668 assert (b > 0);
669
670 /*
671 System.err.println("Scale: original " + image.getWidth() +
672 "x" + image.getHeight());
673 System.err.println(" screen " + (width * textWidth) +
674 "x" + (height * textHeight));
675 System.err.println("A " + a + " B " + b);
676 */
677
678 if (a > b) {
679 // Horizontal letterbox
680 destWidth = width * textWidth;
681 destHeight = (int) (destWidth / a);
682 y = ((height * textHeight) - destHeight) / 2;
683 assert (y >= 0);
684 /*
685 System.err.println("Horizontal letterbox: " + destWidth +
686 "x" + destHeight + ", Y offset " + y);
687 */
688 } else {
689 // Vertical letterbox
690 destHeight = height * textHeight;
691 destWidth = (int) (destHeight * a);
692 x = ((width * textWidth) - destWidth) / 2;
693 assert (x >= 0);
694 /*
695 System.err.println("Vertical letterbox: " + destWidth +
696 "x" + destHeight + ", X offset " + x);
697 */
698 }
699 newImage = new BufferedImage(width * textWidth, height * textHeight,
700 BufferedImage.TYPE_INT_ARGB);
701 break;
702 }
703
704 java.awt.Graphics gr = newImage.createGraphics();
705 if (scale == Scale.SCALE) {
706 gr.setColor(scaleBackColor);
707 gr.fillRect(0, 0, width * textWidth, height * textHeight);
708 }
709 gr.drawImage(image, x, y, destWidth, destHeight, null);
710 gr.dispose();
711 return newImage;
712 }
713
714 /**
715 * Rotate an image either clockwise or counterclockwise.
716 *
717 * @param image the image to scale
718 * @param clockwise number of turns clockwise
719 */
720 private BufferedImage rotateImage(final BufferedImage image,
721 final int clockwise) {
722
723 if (clockwise % 4 == 0) {
724 return image;
725 }
726
727 BufferedImage newImage = null;
728
729 if (clockwise % 4 == 1) {
730 // 90 degrees clockwise
731 newImage = new BufferedImage(image.getHeight(), image.getWidth(),
732 BufferedImage.TYPE_INT_ARGB);
733 for (int x = 0; x < image.getWidth(); x++) {
734 for (int y = 0; y < image.getHeight(); y++) {
735 newImage.setRGB(y, x,
736 image.getRGB(x, image.getHeight() - 1 - y));
737 }
738 }
739 } else if (clockwise % 4 == 2) {
740 // 180 degrees clockwise
741 newImage = new BufferedImage(image.getWidth(), image.getHeight(),
742 BufferedImage.TYPE_INT_ARGB);
743 for (int x = 0; x < image.getWidth(); x++) {
744 for (int y = 0; y < image.getHeight(); y++) {
745 newImage.setRGB(x, y,
746 image.getRGB(image.getWidth() - 1 - x,
747 image.getHeight() - 1 - y));
748 }
749 }
750 } else if (clockwise % 4 == 3) {
751 // 270 degrees clockwise
752 newImage = new BufferedImage(image.getHeight(), image.getWidth(),
753 BufferedImage.TYPE_INT_ARGB);
754 for (int x = 0; x < image.getWidth(); x++) {
755 for (int y = 0; y < image.getHeight(); y++) {
756 newImage.setRGB(y, x,
757 image.getRGB(image.getWidth() - 1 - x, y));
758 }
759 }
760 }
761
762 return newImage;
763 }
764
765 }