X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTImage.java;fp=src%2Fjexer%2FTImage.java;h=cd0ce96e0baf4523c64cb45527c01cbc4d1e1443;hb=12b90437b5f22c2ae6e9b9b14c3b62b60f6143e5;hp=0000000000000000000000000000000000000000;hpb=b709b36e17eb8807819e51297bb398ef28ece52d;p=fanfix.git diff --git a/src/jexer/TImage.java b/src/jexer/TImage.java new file mode 100644 index 0000000..cd0ce96 --- /dev/null +++ b/src/jexer/TImage.java @@ -0,0 +1,765 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer; + +import java.awt.image.BufferedImage; + +import jexer.backend.ECMA48Terminal; +import jexer.backend.MultiScreen; +import jexer.backend.SwingTerminal; +import jexer.bits.Cell; +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import jexer.event.TResizeEvent; +import static jexer.TKeypress.*; + +/** + * TImage renders a piece of a bitmap image on screen. + */ +public class TImage extends TWidget { + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Selections for fitting the image to the text cells. + */ + public enum Scale { + /** + * No scaling. + */ + NONE, + + /** + * Stretch/shrink the image in both directions to fully fill the text + * area width/height. + */ + STRETCH, + + /** + * Scale the image, preserving aspect ratio, to fill the text area + * width/height (like letterbox). The background color for the + * letterboxed area is specified in scaleBackColor. + */ + SCALE, + } + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Scaling strategy to use. + */ + private Scale scale = Scale.NONE; + + /** + * Scaling strategy to use. + */ + private java.awt.Color scaleBackColor = java.awt.Color.BLACK; + + /** + * The action to perform when the user clicks on the image. + */ + private TAction clickAction; + + /** + * The image to display. + */ + private BufferedImage image; + + /** + * The original image from construction time. + */ + private BufferedImage originalImage; + + /** + * The current scaling factor for the image. + */ + private double scaleFactor = 1.0; + + /** + * The current clockwise rotation for the image. + */ + private int clockwise = 0; + + /** + * If true, this widget was resized and a new scaled image must be + * produced. + */ + private boolean resized = false; + + /** + * Left column of the image. 0 is the left-most column. + */ + private int left; + + /** + * Top row of the image. 0 is the top-most row. + */ + private int top; + + /** + * The cells containing the broken up image pieces. + */ + private Cell cells[][]; + + /** + * The number of rows in cells[]. + */ + private int cellRows; + + /** + * The number of columns in cells[]. + */ + private int cellColumns; + + /** + * Last text width value. + */ + private int lastTextWidth = -1; + + /** + * Last text height value. + */ + private int lastTextHeight = -1; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param parent parent widget + * @param x column relative to parent + * @param y row relative to parent + * @param width number of text cells for width of the image + * @param height number of text cells for height of the image + * @param image the image to display + * @param left left column of the image. 0 is the left-most column. + * @param top top row of the image. 0 is the top-most row. + */ + public TImage(final TWidget parent, final int x, final int y, + final int width, final int height, + final BufferedImage image, final int left, final int top) { + + this(parent, x, y, width, height, image, left, top, null); + } + + /** + * Public constructor. + * + * @param parent parent widget + * @param x column relative to parent + * @param y row relative to parent + * @param width number of text cells for width of the image + * @param height number of text cells for height of the image + * @param image the image to display + * @param left left column of the image. 0 is the left-most column. + * @param top top row of the image. 0 is the top-most row. + * @param clickAction function to call when mouse is pressed + */ + public TImage(final TWidget parent, final int x, final int y, + final int width, final int height, + final BufferedImage image, final int left, final int top, + final TAction clickAction) { + + // Set parent and window + super(parent, x, y, width, height); + + setCursorVisible(false); + this.originalImage = image; + this.left = left; + this.top = top; + this.clickAction = clickAction; + + sizeToImage(true); + } + + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Handle mouse press events. + * + * @param mouse mouse button press event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + if (clickAction != null) { + clickAction.DO(this); + return; + } + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + if (!keypress.getKey().isFnKey()) { + if (keypress.getKey().getChar() == '+') { + // Make the image bigger. + scaleFactor *= 1.25; + image = null; + sizeToImage(true); + return; + } + if (keypress.getKey().getChar() == '-') { + // Make the image smaller. + scaleFactor *= 0.80; + image = null; + sizeToImage(true); + return; + } + } + if (keypress.equals(kbAltUp)) { + // Make the image bigger. + scaleFactor *= 1.25; + image = null; + sizeToImage(true); + return; + } + if (keypress.equals(kbAltDown)) { + // Make the image smaller. + scaleFactor *= 0.80; + image = null; + sizeToImage(true); + return; + } + if (keypress.equals(kbAltRight)) { + // Rotate clockwise. + clockwise++; + clockwise %= 4; + image = null; + sizeToImage(true); + return; + } + if (keypress.equals(kbAltLeft)) { + // Rotate counter-clockwise. + clockwise--; + if (clockwise < 0) { + clockwise = 3; + } + image = null; + sizeToImage(true); + return; + } + + if (keypress.equals(kbShiftLeft)) { + switch (scale) { + case NONE: + setScaleType(Scale.SCALE); + return; + case STRETCH: + setScaleType(Scale.NONE); + return; + case SCALE: + setScaleType(Scale.STRETCH); + return; + } + } + if (keypress.equals(kbShiftRight)) { + switch (scale) { + case NONE: + setScaleType(Scale.STRETCH); + return; + case STRETCH: + setScaleType(Scale.SCALE); + return; + case SCALE: + setScaleType(Scale.NONE); + return; + } + } + + // Pass to parent for the things we don't care about. + super.onKeypress(keypress); + } + + /** + * Handle resize events. + * + * @param event resize event + */ + @Override + public void onResize(final TResizeEvent event) { + // Get my width/height set correctly. + super.onResize(event); + + if (scale == Scale.NONE) { + return; + } + image = null; + resized = true; + } + + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw the image. + */ + @Override + public void draw() { + sizeToImage(false); + + // We have already broken the image up, just draw the last set of + // cells. + for (int x = 0; (x < getWidth()) && (x + left < cellColumns); x++) { + if ((left + x) * lastTextWidth > image.getWidth()) { + continue; + } + + for (int y = 0; (y < getHeight()) && (y + top < cellRows); y++) { + if ((top + y) * lastTextHeight > image.getHeight()) { + continue; + } + assert (x + left < cellColumns); + assert (y + top < cellRows); + + getWindow().putCharXY(x, y, cells[x + left][y + top]); + } + } + + } + + // ------------------------------------------------------------------------ + // TImage ----------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Size cells[][] according to the screen font size. + * + * @param always if true, always resize the cells + */ + private void sizeToImage(final boolean always) { + int textWidth = getScreen().getTextWidth(); + int textHeight = getScreen().getTextHeight(); + + if (image == null) { + image = rotateImage(originalImage, clockwise); + image = scaleImage(image, scaleFactor, getWidth(), getHeight(), + textWidth, textHeight); + } + + if ((always == true) || + (resized == true) || + ((textWidth > 0) + && (textWidth != lastTextWidth) + && (textHeight > 0) + && (textHeight != lastTextHeight)) + ) { + resized = false; + + cellColumns = image.getWidth() / textWidth; + if (cellColumns * textWidth < image.getWidth()) { + cellColumns++; + } + cellRows = image.getHeight() / textHeight; + if (cellRows * textHeight < image.getHeight()) { + cellRows++; + } + + // Break the image up into an array of cells. + cells = new Cell[cellColumns][cellRows]; + + for (int x = 0; x < cellColumns; x++) { + for (int y = 0; y < cellRows; y++) { + + int width = textWidth; + if ((x + 1) * textWidth > image.getWidth()) { + width = image.getWidth() - (x * textWidth); + } + int height = textHeight; + if ((y + 1) * textHeight > image.getHeight()) { + height = image.getHeight() - (y * textHeight); + } + + Cell cell = new Cell(); + cell.setImage(image.getSubimage(x * textWidth, + y * textHeight, width, height)); + + cells[x][y] = cell; + } + } + + lastTextWidth = textWidth; + lastTextHeight = textHeight; + } + + if ((left + getWidth()) > cellColumns) { + left = cellColumns - getWidth(); + } + if (left < 0) { + left = 0; + } + if ((top + getHeight()) > cellRows) { + top = cellRows - getHeight(); + } + if (top < 0) { + top = 0; + } + } + + /** + * Get the top corner to render. + * + * @return the top row + */ + public int getTop() { + return top; + } + + /** + * Set the top corner to render. + * + * @param top the new top row + */ + public void setTop(final int top) { + this.top = top; + if (this.top > cellRows - getHeight()) { + this.top = cellRows - getHeight(); + } + if (this.top < 0) { + this.top = 0; + } + } + + /** + * Get the left corner to render. + * + * @return the left column + */ + public int getLeft() { + return left; + } + + /** + * Set the left corner to render. + * + * @param left the new left column + */ + public void setLeft(final int left) { + this.left = left; + if (this.left > cellColumns - getWidth()) { + this.left = cellColumns - getWidth(); + } + if (this.left < 0) { + this.left = 0; + } + } + + /** + * Get the number of text cell rows for this image. + * + * @return the number of rows + */ + public int getRows() { + return cellRows; + } + + /** + * Get the number of text cell columns for this image. + * + * @return the number of columns + */ + public int getColumns() { + return cellColumns; + } + + /** + * Get the raw (unprocessed) image. + * + * @return the image + */ + public BufferedImage getImage() { + return originalImage; + } + + /** + * Set the raw image, and reprocess to make the visible image. + * + * @param image the new image + */ + public void setImage(final BufferedImage image) { + this.originalImage = image; + this.image = null; + sizeToImage(true); + } + + /** + * Get the visible (processed) image. + * + * @return the image that is currently on screen + */ + public BufferedImage getVisibleImage() { + return image; + } + + /** + * Get the scaling strategy. + * + * @return Scale.NONE, Scale.STRETCH, etc. + */ + public Scale getScaleType() { + return scale; + } + + /** + * Set the scaling strategy. + * + * @param scale Scale.NONE, Scale.STRETCH, etc. + */ + public void setScaleType(final Scale scale) { + this.scale = scale; + this.image = null; + sizeToImage(true); + } + + /** + * Get the scale factor. + * + * @return the scale factor + */ + public double getScaleFactor() { + return scaleFactor; + } + + /** + * Set the scale factor. 1.0 means no scaling. + * + * @param scaleFactor the new scale factor + */ + public void setScaleFactor(final double scaleFactor) { + this.scaleFactor = scaleFactor; + image = null; + sizeToImage(true); + } + + /** + * Get the rotation, as degrees. + * + * @return the rotation in degrees + */ + public int getRotation() { + switch (clockwise) { + case 0: + return 0; + case 1: + return 90; + case 2: + return 180; + case 3: + return 270; + default: + // Don't know how this happened, but fix it. + clockwise = 0; + image = null; + sizeToImage(true); + return 0; + } + } + + /** + * Set the rotation, as degrees clockwise. + * + * @param rotation 0, 90, 180, or 270 + */ + public void setRotation(final int rotation) { + switch (rotation) { + case 0: + clockwise = 0; + break; + case 90: + clockwise = 1; + break; + case 180: + clockwise = 2; + break; + case 270: + clockwise = 3; + break; + default: + // Don't know how this happened, but fix it. + clockwise = 0; + break; + } + + image = null; + sizeToImage(true); + } + + /** + * Scale an image by to be scaleFactor size. + * + * @param image the image to scale + * @param factor the scale to make the new image + * @param width the number of text cell columns for the destination image + * @param height the number of text cell rows for the destination image + * @param textWidth the width in pixels for one text cell + * @param textHeight the height in pixels for one text cell + */ + private BufferedImage scaleImage(final BufferedImage image, + final double factor, final int width, final int height, + final int textWidth, final int textHeight) { + + if ((scale == Scale.NONE) && (Math.abs(factor - 1.0) < 0.03)) { + // If we are within 3% of 1.0, just return the original image. + return image; + } + + int destWidth = 0; + int destHeight = 0; + int x = 0; + int y = 0; + + BufferedImage newImage = null; + + switch (scale) { + case NONE: + destWidth = (int) (image.getWidth() * factor); + destHeight = (int) (image.getHeight() * factor); + newImage = new BufferedImage(destWidth, destHeight, + BufferedImage.TYPE_INT_ARGB); + break; + case STRETCH: + destWidth = width * textWidth; + destHeight = height * textHeight; + newImage = new BufferedImage(destWidth, destHeight, + BufferedImage.TYPE_INT_ARGB); + break; + case SCALE: + double a = (double) image.getWidth() / image.getHeight(); + double b = (double) (width * textWidth) / (height * textHeight); + assert (a > 0); + assert (b > 0); + + /* + System.err.println("Scale: original " + image.getWidth() + + "x" + image.getHeight()); + System.err.println(" screen " + (width * textWidth) + + "x" + (height * textHeight)); + System.err.println("A " + a + " B " + b); + */ + + if (a > b) { + // Horizontal letterbox + destWidth = width * textWidth; + destHeight = (int) (destWidth / a); + y = ((height * textHeight) - destHeight) / 2; + assert (y >= 0); + /* + System.err.println("Horizontal letterbox: " + destWidth + + "x" + destHeight + ", Y offset " + y); + */ + } else { + // Vertical letterbox + destHeight = height * textHeight; + destWidth = (int) (destHeight * a); + x = ((width * textWidth) - destWidth) / 2; + assert (x >= 0); + /* + System.err.println("Vertical letterbox: " + destWidth + + "x" + destHeight + ", X offset " + x); + */ + } + newImage = new BufferedImage(width * textWidth, height * textHeight, + BufferedImage.TYPE_INT_ARGB); + break; + } + + java.awt.Graphics gr = newImage.createGraphics(); + if (scale == Scale.SCALE) { + gr.setColor(scaleBackColor); + gr.fillRect(0, 0, width * textWidth, height * textHeight); + } + gr.drawImage(image, x, y, destWidth, destHeight, null); + gr.dispose(); + return newImage; + } + + /** + * Rotate an image either clockwise or counterclockwise. + * + * @param image the image to scale + * @param clockwise number of turns clockwise + */ + private BufferedImage rotateImage(final BufferedImage image, + final int clockwise) { + + if (clockwise % 4 == 0) { + return image; + } + + BufferedImage newImage = null; + + if (clockwise % 4 == 1) { + // 90 degrees clockwise + newImage = new BufferedImage(image.getHeight(), image.getWidth(), + BufferedImage.TYPE_INT_ARGB); + for (int x = 0; x < image.getWidth(); x++) { + for (int y = 0; y < image.getHeight(); y++) { + newImage.setRGB(y, x, + image.getRGB(x, image.getHeight() - 1 - y)); + } + } + } else if (clockwise % 4 == 2) { + // 180 degrees clockwise + newImage = new BufferedImage(image.getWidth(), image.getHeight(), + BufferedImage.TYPE_INT_ARGB); + for (int x = 0; x < image.getWidth(); x++) { + for (int y = 0; y < image.getHeight(); y++) { + newImage.setRGB(x, y, + image.getRGB(image.getWidth() - 1 - x, + image.getHeight() - 1 - y)); + } + } + } else if (clockwise % 4 == 3) { + // 270 degrees clockwise + newImage = new BufferedImage(image.getHeight(), image.getWidth(), + BufferedImage.TYPE_INT_ARGB); + for (int x = 0; x < image.getWidth(); x++) { + for (int y = 0; y < image.getHeight(); y++) { + newImage.setRGB(y, x, + image.getRGB(image.getWidth() - 1 - x, y)); + } + } + } + + return newImage; + } + +}