From 52c7290c1d1a4f73df6f212fec87f8ae4d3a7248 Mon Sep 17 00:00:00 2001 From: Svjatoslav Agejenko Date: Sun, 5 Jan 2020 18:59:00 +0200 Subject: [PATCH] Quickly hacked together capability to thumbnail animated gifs based on code snippets found on the internet. Needs refactoring. --- .../meviz/htmlindexer/GifSequenceWriter.java | 77 ++++++++++++ .../meviz/htmlindexer/ImageFormatError.java | 2 +- .../svjatoslav/meviz/htmlindexer/Utils.java | 115 ++++++++++++++++++ .../htmlindexer/indexer/AbstractIndexer.java | 7 +- .../metadata/fileTypes/Picture.java | 72 ++++++++++- 5 files changed, 262 insertions(+), 11 deletions(-) create mode 100644 src/main/java/eu/svjatoslav/meviz/htmlindexer/GifSequenceWriter.java diff --git a/src/main/java/eu/svjatoslav/meviz/htmlindexer/GifSequenceWriter.java b/src/main/java/eu/svjatoslav/meviz/htmlindexer/GifSequenceWriter.java new file mode 100644 index 0000000..c62bc23 --- /dev/null +++ b/src/main/java/eu/svjatoslav/meviz/htmlindexer/GifSequenceWriter.java @@ -0,0 +1,77 @@ +package eu.svjatoslav.meviz.htmlindexer; + +import javax.imageio.*; +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.stream.ImageOutputStream; +import java.awt.image.RenderedImage; +import java.io.IOException; + +/** + * Source: https://memorynotfound.com/generate-gif-image-java-delay-infinite-loop-example/ + */ +public class GifSequenceWriter { + protected ImageWriter writer; + protected ImageWriteParam params; + protected IIOMetadata metadata; + + public GifSequenceWriter(ImageOutputStream out, int imageType, int delay, boolean loop) throws IOException { + writer = ImageIO.getImageWritersBySuffix("gif").next(); + params = writer.getDefaultWriteParam(); + + ImageTypeSpecifier imageTypeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(imageType); + metadata = writer.getDefaultImageMetadata(imageTypeSpecifier, params); + + configureRootMetadata(delay, loop); + + writer.setOutput(out); + writer.prepareWriteSequence(null); + } + + private void configureRootMetadata(int delay, boolean loop) throws IIOInvalidTreeException { + String metaFormatName = metadata.getNativeMetadataFormatName(); + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(metaFormatName); + + IIOMetadataNode graphicsControlExtensionNode = getNode(root, "GraphicControlExtension"); + graphicsControlExtensionNode.setAttribute("disposalMethod", "none"); + graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE"); + graphicsControlExtensionNode.setAttribute("transparentColorFlag", "FALSE"); + graphicsControlExtensionNode.setAttribute("delayTime", Integer.toString(delay / 10)); + graphicsControlExtensionNode.setAttribute("transparentColorIndex", "0"); + + IIOMetadataNode commentsNode = getNode(root, "CommentExtensions"); + commentsNode.setAttribute("CommentExtension", "Created by: https://memorynotfound.com"); + + IIOMetadataNode appExtensionsNode = getNode(root, "ApplicationExtensions"); + IIOMetadataNode child = new IIOMetadataNode("ApplicationExtension"); + child.setAttribute("applicationID", "NETSCAPE"); + child.setAttribute("authenticationCode", "2.0"); + + int loopContinuously = loop ? 0 : 1; + child.setUserObject(new byte[]{ 0x1, (byte) (loopContinuously & 0xFF), (byte) ((loopContinuously >> 8) & 0xFF)}); + appExtensionsNode.appendChild(child); + metadata.setFromTree(metaFormatName, root); + } + + private static IIOMetadataNode getNode(IIOMetadataNode rootNode, String nodeName){ + int nNodes = rootNode.getLength(); + for (int i = 0; i < nNodes; i++){ + if (rootNode.item(i).getNodeName().equalsIgnoreCase(nodeName)){ + return (IIOMetadataNode) rootNode.item(i); + } + } + IIOMetadataNode node = new IIOMetadataNode(nodeName); + rootNode.appendChild(node); + return(node); + } + + public void writeToSequence(RenderedImage img) throws IOException { + writer.writeToSequence(new IIOImage(img, null, metadata), params); + } + + public void close() throws IOException { + writer.endWriteSequence(); + } + +} diff --git a/src/main/java/eu/svjatoslav/meviz/htmlindexer/ImageFormatError.java b/src/main/java/eu/svjatoslav/meviz/htmlindexer/ImageFormatError.java index 605b254..9f8349f 100755 --- a/src/main/java/eu/svjatoslav/meviz/htmlindexer/ImageFormatError.java +++ b/src/main/java/eu/svjatoslav/meviz/htmlindexer/ImageFormatError.java @@ -9,7 +9,7 @@ package eu.svjatoslav.meviz.htmlindexer; -class ImageFormatError extends Exception { +public class ImageFormatError extends Exception { private static final long serialVersionUID = 4037233564457071385L; diff --git a/src/main/java/eu/svjatoslav/meviz/htmlindexer/Utils.java b/src/main/java/eu/svjatoslav/meviz/htmlindexer/Utils.java index 01f2d37..ba0d23c 100755 --- a/src/main/java/eu/svjatoslav/meviz/htmlindexer/Utils.java +++ b/src/main/java/eu/svjatoslav/meviz/htmlindexer/Utils.java @@ -11,12 +11,20 @@ package eu.svjatoslav.meviz.htmlindexer; import eu.svjatoslav.meviz.htmlindexer.layouts.Layout; import eu.svjatoslav.meviz.htmlindexer.layouts.MixedLayout; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import java.awt.*; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.util.ArrayList; import java.util.HashSet; import java.util.zip.CRC32; @@ -139,4 +147,111 @@ public class Utils { // throw new RuntimeException(e); // } } + + public static ImageFrame[] readGIF(ImageReader reader) throws IOException { + ArrayList frames = new ArrayList(2); + + int width = -1; + int height = -1; + + IIOMetadata metadata = reader.getStreamMetadata(); + if (metadata != null) { + IIOMetadataNode globalRoot = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName()); + + NodeList globalScreenDescriptor = globalRoot.getElementsByTagName("LogicalScreenDescriptor"); + + if (globalScreenDescriptor != null && globalScreenDescriptor.getLength() > 0) { + IIOMetadataNode screenDescriptor = (IIOMetadataNode) globalScreenDescriptor.item(0); + + if (screenDescriptor != null) { + width = Integer.parseInt(screenDescriptor.getAttribute("logicalScreenWidth")); + height = Integer.parseInt(screenDescriptor.getAttribute("logicalScreenHeight")); + } + } + } + + BufferedImage master = null; + Graphics2D masterGraphics = null; + + for (int frameIndex = 0;; frameIndex++) { + BufferedImage image; + try { + image = reader.read(frameIndex); + } catch (IndexOutOfBoundsException io) { + break; + } + + if (width == -1 || height == -1) { + width = image.getWidth(); + height = image.getHeight(); + } + + IIOMetadataNode root = (IIOMetadataNode) reader.getImageMetadata(frameIndex).getAsTree("javax_imageio_gif_image_1.0"); + IIOMetadataNode gce = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0); + int delay = Integer.valueOf(gce.getAttribute("delayTime")); + String disposal = gce.getAttribute("disposalMethod"); + + int x = 0; + int y = 0; + + if (master == null) { + master = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + masterGraphics = master.createGraphics(); + masterGraphics.setBackground(new Color(0, 0, 0, 0)); + } else { + NodeList children = root.getChildNodes(); + for (int nodeIndex = 0; nodeIndex < children.getLength(); nodeIndex++) { + Node nodeItem = children.item(nodeIndex); + if (nodeItem.getNodeName().equals("ImageDescriptor")) { + NamedNodeMap map = nodeItem.getAttributes(); + x = Integer.valueOf(map.getNamedItem("imageLeftPosition").getNodeValue()); + y = Integer.valueOf(map.getNamedItem("imageTopPosition").getNodeValue()); + } + } + } + masterGraphics.drawImage(image, x, y, null); + + BufferedImage copy = new BufferedImage(master.getColorModel(), master.copyData(null), master.isAlphaPremultiplied(), null); + frames.add(new ImageFrame(copy, delay, disposal)); + + if (disposal.equals("restoreToPrevious")) { + BufferedImage from = null; + for (int i = frameIndex - 1; i >= 0; i--) { + if (!frames.get(i).getDisposal().equals("restoreToPrevious") || frameIndex == 0) { + from = frames.get(i).image; + break; + } + } + + master = new BufferedImage(from.getColorModel(), from.copyData(null), from.isAlphaPremultiplied(), null); + masterGraphics = master.createGraphics(); + masterGraphics.setBackground(new Color(0, 0, 0, 0)); + } else if (disposal.equals("restoreToBackgroundColor")) { + masterGraphics.clearRect(x, y, image.getWidth(), image.getHeight()); + } + } + reader.dispose(); + + return frames.toArray(new ImageFrame[frames.size()]); + } + + public static class ImageFrame { + private final int delay; + public BufferedImage image; + private final String disposal; + + public ImageFrame(BufferedImage image, int delay, String disposal) { + this.image = image; + this.delay = delay; + this.disposal = disposal; + } + + public int getDelay() { + return delay; + } + + public String getDisposal() { + return disposal; + } + } } diff --git a/src/main/java/eu/svjatoslav/meviz/htmlindexer/indexer/AbstractIndexer.java b/src/main/java/eu/svjatoslav/meviz/htmlindexer/indexer/AbstractIndexer.java index 0329989..258f2f1 100644 --- a/src/main/java/eu/svjatoslav/meviz/htmlindexer/indexer/AbstractIndexer.java +++ b/src/main/java/eu/svjatoslav/meviz/htmlindexer/indexer/AbstractIndexer.java @@ -11,14 +11,13 @@ import java.io.UnsupportedEncodingException; import static eu.svjatoslav.meviz.htmlindexer.Constants.SUPPORTED_IMAGE_EXTENSIONS; import static eu.svjatoslav.meviz.htmlindexer.Constants.SUPPORTED_VIDEO_EXTENSIONS; +import static java.util.Arrays.stream; public abstract class AbstractIndexer { public static boolean isImage(final String fileExtension) { - for (final String ext : SUPPORTED_IMAGE_EXTENSIONS) - if (ext.equals(fileExtension)) - return true; - return false; + return stream(SUPPORTED_IMAGE_EXTENSIONS) + .anyMatch(ext -> ext.equals(fileExtension)); } public static boolean isVideo(final String fileExtension) { diff --git a/src/main/java/eu/svjatoslav/meviz/htmlindexer/metadata/fileTypes/Picture.java b/src/main/java/eu/svjatoslav/meviz/htmlindexer/metadata/fileTypes/Picture.java index 9f35743..7a1ba39 100755 --- a/src/main/java/eu/svjatoslav/meviz/htmlindexer/metadata/fileTypes/Picture.java +++ b/src/main/java/eu/svjatoslav/meviz/htmlindexer/metadata/fileTypes/Picture.java @@ -10,20 +10,25 @@ package eu.svjatoslav.meviz.htmlindexer.metadata.fileTypes; import eu.svjatoslav.commons.file.FilePathParser; +import eu.svjatoslav.commons.file.IOHelper; import eu.svjatoslav.meviz.htmlindexer.Constants; +import eu.svjatoslav.meviz.htmlindexer.GifSequenceWriter; +import eu.svjatoslav.meviz.htmlindexer.ImageFormatError; import eu.svjatoslav.meviz.htmlindexer.Utils; import eu.svjatoslav.meviz.htmlindexer.metadata.Dimension; import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.FileImageOutputStream; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.FilteredImageSource; import java.awt.image.ImageFilter; import java.awt.image.ImageProducer; -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStream; +import java.io.*; import java.util.ArrayList; import java.util.List; @@ -106,7 +111,21 @@ public class Picture extends AbstractFile { final File outputFile, final java.awt.Dimension preferredTargetDimensions) { + String fileExtension = FilePathParser.getFileExtension(inputFile.getName()); + try { + if ("gif".equalsIgnoreCase(fileExtension)) + makeGifThumbnail(inputFile, outputFile, preferredTargetDimensions); + else + makeJpegThumbnail(inputFile, outputFile, preferredTargetDimensions); + } catch (final Exception exception) { + System.out.println(exception.toString()); + exception.printStackTrace(); + } + } + + private static void makeJpegThumbnail(File inputFile, File outputFile, java.awt.Dimension preferredTargetDimensions) + throws IOException, ImageFormatError { final BufferedImage inputImage = getBufferedImage(inputFile); @@ -131,11 +150,52 @@ public class Picture extends AbstractFile { ImageIO.write(bufferedImage, "jpg", out); out.close(); + } - } catch (final Exception exception) { - System.out.println(exception.toString()); - exception.printStackTrace(); + private static void makeGifThumbnail( + File inputFile, File outputFile, java.awt.Dimension preferredTargetDimensions) throws IOException { + ImageIcon imageIcon = new ImageIcon(IOHelper.getFileContents(inputFile)); + + final java.awt.Dimension sourceImageDimension = new java.awt.Dimension( + imageIcon.getIconWidth(), imageIcon.getIconHeight()); + + System.out.println("Source image dimensions:" + sourceImageDimension); + + final java.awt.Dimension targetDimensions = getTargetThumbnailDimension( + sourceImageDimension, preferredTargetDimensions); + + System.out.println("Desired target image dimensions:" + targetDimensions); + + FileInputStream fiStream = new FileInputStream( inputFile ); + + ImageReader reader = ImageIO.getImageReadersByFormatName("gif").next(); + ImageInputStream stream = ImageIO.createImageInputStream(inputFile); + reader.setInput(stream); + + Utils.ImageFrame[] frames = Utils.readGIF(reader); + for (Utils.ImageFrame frame : frames) { + Image scaleImage = scaleImage(frame.image, targetDimensions.width, targetDimensions.height); + BufferedImage bimage = new BufferedImage( + targetDimensions.width, targetDimensions.height, BufferedImage.TYPE_INT_ARGB); + Graphics2D bGr = bimage.createGraphics(); + bGr.drawImage(scaleImage, 0, 0, null); + bGr.dispose(); + frame.image = bimage; } + + ImageOutputStream output = new FileImageOutputStream(outputFile ); + + GifSequenceWriter writer = + new GifSequenceWriter( output, frames[0].image.getType(), frames[0].getDelay(), true ); + + writer.writeToSequence( frames[0].image ); + for ( int i = 1; i < frames.length; i++ ) { + BufferedImage nextImage = frames[i].image; + writer.writeToSequence( nextImage ); + } + + writer.close(); + output.close(); } /** -- 2.20.1