diff --git a/src/commands/CommandYeet.java b/src/commands/CommandYeet.java index 7b29add..33eda5d 100644 --- a/src/commands/CommandYeet.java +++ b/src/commands/CommandYeet.java @@ -1,24 +1,8 @@ package commands; -import java.awt.image.BufferedImage; -import java.awt.image.DataBufferByte; -import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; -import java.util.Calendar; -import java.util.Iterator; - -import javax.imageio.IIOException; -import javax.imageio.IIOImage; import javax.imageio.ImageIO; -import javax.imageio.ImageTypeSpecifier; -import javax.imageio.ImageWriteParam; -import javax.imageio.ImageWriter; -import javax.imageio.metadata.IIOMetadata; -import javax.imageio.metadata.IIOMetadataNode; -import javax.imageio.stream.FileImageOutputStream; -import javax.imageio.stream.ImageOutputStream; - import core.Command; import core.Localizer; import dataStructures.KittyChannel; @@ -28,6 +12,7 @@ import dataStructures.KittyRole; import dataStructures.KittyUser; import dataStructures.Response; import dataStructures.UserInput; +import utils.ImageOverlayBuilder; import utils.ImageUtils; public class CommandYeet extends Command @@ -73,7 +58,8 @@ public class CommandYeet extends Command return; yeeteeFile = new File(yeeteeFilename); - YEET(ImageIO.read(yeeteeFile), name); + ImageOverlayBuilder builder = new ImageOverlayBuilder("assets/yeet/frames/", "yeet ", 24, 18); + builder.Overlay(ImageIO.read(yeeteeFile), name); } catch (IOException e) { @@ -87,251 +73,4 @@ public class CommandYeet extends Command ImageUtils.BlockingFileDelete(yeetFile); ImageUtils.BlockingFileDelete(yeeteeFile); } - - // Lets get some trash established for the yeet - public static final boolean VERBOSE = false; - public static final int NONE = -1; - public static final int PIXEL_BYTE_LENGTH = 4; // 32-bit PNG, RGBA - public static final String YEET_BASE_PATH = "assets/yeet/frames/"; - public static final int YEET_BASE_SIZE = 24; - public static final int YEET_FPS = 18; - - // Some small logging functions... cause we're professionals... - public static void Verbose(String str) { if(VERBOSE) Log("[Verbose] " + str); } - public static void Log(String str) { System.out.println("[Log] " + str); } - public static void Warn(String str) { System.out.println("[Warn] " + str); } - public static void Error(String str) { System.out.println("[Error] " + str); } - - // Performs the Y E E T (image overlay per frame specified, catches IO errors as a note.) - public static void YEET(BufferedImage overlay, String outpath) - { - try - { - long start = Calendar.getInstance().getTimeInMillis(); - - ProcessYeet(overlay, outpath); - - long end = Calendar.getInstance().getTimeInMillis(); - Log("Took " + (end - start) + "ms"); - } - catch (IOException e) - { - e.printStackTrace(); - } - } - - // Processing the image frames to construct a gif. Relies on (0,255,0) pixel for image center specification. - public static void ProcessYeet(BufferedImage overlay, String outfileName) throws IOException - { - BufferedImage[] frames = new BufferedImage[YEET_BASE_SIZE]; - - for(int i = 0; i < YEET_BASE_SIZE; ++i) - { - BufferedImage out = CombineImages(YEET_BASE_PATH + "yeet " + i + ".png", overlay); - - if(out != null) - frames[i] = out; - else - Error("There was an issue with frame " + i); - } - - Verbose("Processed " + YEET_BASE_SIZE + " frames, writing..."); - ImageOutputStream output = new FileImageOutputStream(new File(outfileName)); - GifSequenceWriter writer = new GifSequenceWriter(output, frames[0].getType(), YEET_FPS, true); - - for(int i = 0; i < YEET_BASE_SIZE; ++i) - writer.writeToSequence(frames[i]); - - writer.close(); - output.close(); - } - - // Performs an overlay at a pixel location. We're making some assumptions here, mostly that there is going - // to be an RGBA-32-bit encoded PNG image read in for our parsing purposes. We then look for a 0, 255, 0 - // green pixel on the base frame to apply the overlay buffer to, centered. - public static BufferedImage CombineImages(String pathBase, BufferedImage overlay) throws IOException - { - // Acquire images - BufferedImage imageBase = null; - BufferedImage imageOverlay = overlay; - - imageBase = ImageIO.read(new File(pathBase)); - - // Grab data we know won't change. As a side note, I discovered you can get specific pixels a bit - // differently later, but it was too late, I wrote the pixel specific code already. - final byte[] pixelsBase = ((DataBufferByte) imageBase.getRaster().getDataBuffer()).getData(); - final int baseWidth = imageBase.getWidth(); - final int overlayWidth = imageOverlay.getWidth(); - final int baseHeight = imageBase.getHeight(); - final int overlayHeight = imageOverlay.getHeight(); - - // Warn about odd sizing when applicable - if(overlayHeight > baseHeight || overlayWidth > baseWidth) - Warn("Size mismatch, overlay is larger. May function, but not supported."); - - // Skim base picture for solid green pixel to use as target center - int targetX = NONE; - int targetY = NONE; - for(int i = 0; i < pixelsBase.length; i += PIXEL_BYTE_LENGTH) - { - final int x = ((i / PIXEL_BYTE_LENGTH) % baseWidth); - final int y = ((i / PIXEL_BYTE_LENGTH) / baseWidth); - - final int r = (pixelsBase[i]); - final int g = (pixelsBase[i + 1]); - final int b = (pixelsBase[i + 2]); - //final int a = (pixelsBase[i + 3]); - - // Max contrast in 32-bit PNG for green is no red no blue, max green (ratio), max alpha (implied 0 here) - if(r == -1 && g == 0 && b == -1) - { - //Log("Found target pixel at x: " + x + " y: " + y); - targetX = x; - targetY = y; - break; - } - } - - // Skip overlay if we have no target location - if(targetX == NONE || targetY == NONE) - { - Verbose("No overlay required for this frame."); - return imageBase; - } - - // Re-create a new byte array and establish overlay bounds - final int left = targetX - overlayWidth / 2; - final int right = targetX + overlayWidth / 2; - final int top = targetY - overlayHeight / 2; - final int bottom = targetY + overlayHeight / 2; - - // Apply overlay when appropriate - for(int i = 0; i < pixelsBase.length; i += PIXEL_BYTE_LENGTH) - { - final int x = ((i / PIXEL_BYTE_LENGTH) % baseWidth); - final int y = ((i / PIXEL_BYTE_LENGTH) / baseWidth); - - // If we're within the bounds of where the overlay picture should go... - if(x > left && x < right && y > top && y < bottom) - { - final int overlayX = x - left; - final int overlayY = y - top; - - // Skip out of bounds pixels - if(overlayX < 0 || overlayY < 0 || overlayX >= baseWidth || overlayY >= baseHeight) - continue; - - // Re-write bytes as overlay - imageBase.setRGB(x, y, imageOverlay.getRGB(overlayX, overlayY)); - } - } - - // Give the frame back - return imageBase; - } - - - // GifSequenceWriter.java - // - // Created by Elliot Kroo on 2009-04-25, - // (Small modifications by wisp in 2018) - // - // This work is licensed under the Creative Commons Attribution 3.0 Unported License. - // To view a copy of this license visit http://creativecommons.org/licenses/by/3.0/ - // NOTE(wisp): This is Elliot's original license - public static class GifSequenceWriter - { - protected ImageWriter gifWriter; - protected ImageWriteParam imageWriteParam; - protected IIOMetadata imageMetaData; - - // NOTE: This is limited to things that divide into 1000 cleanly as multiples of 10. - public GifSequenceWriter(ImageOutputStream outputStream, int imageType, int framesPerSecond, boolean isLooping) throws IIOException, IOException - { - gifWriter = getWriter(); - imageWriteParam = gifWriter.getDefaultWriteParam(); - ImageTypeSpecifier imageTypeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(imageType); - - imageMetaData = gifWriter.getDefaultImageMetadata(imageTypeSpecifier, imageWriteParam); - - String metaFormatName = imageMetaData.getNativeMetadataFormatName(); - - IIOMetadataNode root = (IIOMetadataNode) imageMetaData.getAsTree(metaFormatName); - IIOMetadataNode graphicsControlExtensionNode = getNode(root, "GraphicControlExtension"); - graphicsControlExtensionNode.setAttribute("disposalMethod", "none"); - graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE"); - graphicsControlExtensionNode.setAttribute("transparentColorFlag", "FALSE"); - graphicsControlExtensionNode.setAttribute("delayTime", "" + (int) ((1000.0 / framesPerSecond) / 10.0)); // THis line isn't super well honored - graphicsControlExtensionNode.setAttribute("transparentColorIndex","0"); - - IIOMetadataNode appEntensionsNode = getNode( root, "ApplicationExtensions"); - IIOMetadataNode child = new IIOMetadataNode("ApplicationExtension"); - child.setAttribute("applicationID", "NETSCAPE"); - child.setAttribute("authenticationCode", "2.0"); - - int loopValue = isLooping ? 0 : 1; - child.setUserObject(new byte[]{ 0x1, (byte) (loopValue & 0xFF), (byte) ((loopValue >> 8) & 0xFF)}); - appEntensionsNode.appendChild(child); - - imageMetaData.setFromTree(metaFormatName, root); - - gifWriter.setOutput(outputStream); - gifWriter.prepareWriteSequence(null); - } - - public void writeToSequence(RenderedImage img) throws IOException - { - gifWriter.writeToSequence(new IIOImage(img, null, imageMetaData), imageWriteParam); - } - - /** - * Close this GifSequenceWriter object. This does not close the underlying - * stream, just finishes off the GIF. - */ - public void close() throws IOException - { - gifWriter.endWriteSequence(); - } - - /** - * Returns the first available GIF ImageWriter using - * ImageIO.getImageWritersBySuffix("gif"). - * - * @return a GIF ImageWriter object - * @throws IIOException if no GIF image writers are returned - */ - private static ImageWriter getWriter() throws IIOException - { - Iterator iter = ImageIO.getImageWritersBySuffix("gif"); - - if(!iter.hasNext()) - throw new IIOException("No GIF Image Writers Exist"); - else - return iter.next(); - } - - /** - * Returns an existing child node, or creates and returns a new child node (if - * the requested node does not exist). - * - * @param rootNode the IIOMetadataNode to search for the child node. - * @param nodeName the name of the child node. - * - * @return the child node, if found or a new node created with the given name. - */ - private static IIOMetadataNode getNode(IIOMetadataNode rootNode, String nodeName) - { - int nodeCount = rootNode.getLength(); - for (int i = 0; i < nodeCount; i++) - { - if (rootNode.item(i).getNodeName().compareToIgnoreCase(nodeName) == 0) - return((IIOMetadataNode) rootNode.item(i)); - } - - IIOMetadataNode node = new IIOMetadataNode(nodeName); - rootNode.appendChild(node); - - return(node); - } - } } diff --git a/src/utils/ImageOverlayHelper.java b/src/utils/ImageOverlayBuilder.java similarity index 91% rename from src/utils/ImageOverlayHelper.java rename to src/utils/ImageOverlayBuilder.java index e1a5855..2ad0546 100644 --- a/src/utils/ImageOverlayHelper.java +++ b/src/utils/ImageOverlayBuilder.java @@ -21,14 +21,15 @@ import javax.imageio.stream.ImageOutputStream; // A class that overlays an image based on a series of frames that will be converted into a gif. // The class targets a pixel with max Green value and no red or blue that's fully opaque (0, 255, 0) -// and then places the center of the overlay image on the specified location. -public class ImageOverlayHelper +// and then places the center of the overlay image on the specified location. Must be 32-bit PNGs with RGBA format. +public class ImageOverlayBuilder { // Lets get some trash established for the yeet public static final boolean VERBOSE = false; public static final int NONE = -1; public static final int PIXEL_BYTE_LENGTH = 4; // 32-bit PNG, RGBA - public static final int FPS = 18; // This doesn't quite line up, multiples of 10 are as specific as it gets. + public static final int DEFAULT_FPS = 18; + public static final String FRAME_FILE_EXTENSION = ".png"; public static final String LOG_HEADER = "[Overlay] "; // Logging intermediate functions @@ -41,13 +42,21 @@ public class ImageOverlayHelper private final String basePath; // The folder location of the files private final String baseName; // This is the consistent part of the filename of each frame, for example "image " private final int frameCount; // The number of frames in the image + private final int fps; // This doesn't quite line up, multiples of 10 are as specific as it gets. - // Constructor - public ImageOverlayHelper(String basePath, String baseName, int frameCount) + // Required minimal constructor + public ImageOverlayBuilder(String basePath, String baseName, int frameCount) + { + this(basePath, baseName, frameCount, DEFAULT_FPS); + } + + // Constructor with framerate specification + public ImageOverlayBuilder(String basePath, String baseName, int frameCount, int fps) { this.basePath = basePath; this.baseName = baseName; this.frameCount = frameCount; + this.fps = fps; } // Performs a per-frame overlay, catches IO errors as a note.) @@ -69,13 +78,13 @@ public class ImageOverlayHelper } // Processing the image frames to construct a gif. Relies on (0,255,0) pixel for image center specification. - public void ProcessOverlay(BufferedImage overlay, String outfileName) throws IOException + private void ProcessOverlay(BufferedImage overlay, String outfileName) throws IOException { BufferedImage[] frames = new BufferedImage[frameCount]; for(int i = 0; i < frameCount; ++i) { - BufferedImage out = CombineImages(basePath + baseName + i + ".png", overlay); + BufferedImage out = CombineImages(basePath + baseName + i + FRAME_FILE_EXTENSION , overlay); if(out != null) frames[i] = out; @@ -85,7 +94,7 @@ public class ImageOverlayHelper Verbose("Processed " + frameCount + " frames, writing..."); ImageOutputStream output = new FileImageOutputStream(new File(outfileName)); - GifSequenceWriter writer = new GifSequenceWriter(output, frames[0].getType(), FPS, true); + GifSequenceWriter writer = new GifSequenceWriter(output, frames[0].getType(), fps, true); for(int i = 0; i < frameCount; ++i) writer.writeToSequence(frames[i]); @@ -97,7 +106,7 @@ public class ImageOverlayHelper // Performs an overlay at a pixel location. We're making some assumptions here, mostly that there is going // to be an RGBA-32-bit encoded PNG image read in for our parsing purposes. We then look for a 0, 255, 0 // green pixel on the base frame to apply the overlay buffer to, centered. - public static BufferedImage CombineImages(String pathBase, BufferedImage overlay) throws IOException + private BufferedImage CombineImages(String pathBase, BufferedImage overlay) throws IOException { // Acquire images BufferedImage imageBase = null; @@ -187,7 +196,7 @@ public class ImageOverlayHelper // This work is licensed under the Creative Commons Attribution 3.0 Unported License. // To view a copy of this license visit http://creativecommons.org/licenses/by/3.0/ // NOTE: ^ This is Elliot's original license - public static class GifSequenceWriter + private static class GifSequenceWriter { protected ImageWriter gifWriter; protected ImageWriteParam imageWriteParam;