Skip to content

Commit

Permalink
Add animated GIF writer, prep version 0.1.4.
Browse files Browse the repository at this point in the history
  • Loading branch information
brunchboy committed Mar 25, 2016
1 parent 16d1252 commit 52ac537
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 4 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Expand Up @@ -6,8 +6,15 @@ This change log follows the conventions of

## [Unreleased][unreleased]

Nothing so far.

## [0.1.4] - 2016-03-24

### Added

- Incorporated an [Animated GIF Writer](http://elliot.kroo.net/software/java/GifSequenceWriter/)
to make it easy to capture interfaces that are being rendered on the
push, to facilitate great online documentation.
- When packaging, an überjar containing all transitive dependencies is
also built, to make life easier for people who want to use Wayang
without Maven, Leiningen, or another dependency management system.
Expand Down Expand Up @@ -54,6 +61,7 @@ This change log follows the conventions of
- Initial Public Release


[unreleased]: https://github.com/brunchboy/wayang/compare/v0.1.3...HEAD
[unreleased]: https://github.com/brunchboy/wayang/compare/v0.1.4...HEAD
[0.1.4]: https://github.com/brunchboy/wayang/compare/v0.1.3...v0.1.4
[0.1.3]: https://github.com/brunchboy/wayang/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/brunchboy/wayang/compare/v0.1.1...v0.1.2
43 changes: 41 additions & 2 deletions README.md
Expand Up @@ -86,15 +86,54 @@ This photo shows the results of running `mvn test` in the project directory:

![Test display](assets/Test.jpg)

## Capturing Your Interface

You can save animated GIF files to provide highly meaningful documentation for
your user interfaces, thanks to an embedded version of Elliot Kroo’s
[GifSequenceWriter](http://elliot.kroo.net/software/java/GifSequenceWriter/).
To do that, just call:

```java
import org.deepsymmetry.GifSequenceWriter;

// ...

ImageOutputStream output =
new FileImageOutputStream(new File("out.gif"));

// Pass in the displayImage buffer that Wayang.open() gave you, the
// interval in milliseconds at which you update the display, and
// whether you want the animation to loop:
GifSequenceWriter writer =
new GifSequenceWriter(output, displayImage, 50, true);

// Then whenever you are calling Wayang.SendFrame(), also call:

writer.writeToSequence(displayImage);

// When you are done recording, call:

writer.close();
output.close();

```

Here is an example recording from [Afterglow](https://github.com/brunchboy/afterglow#afterglow)’s
Push 2 interface:

![Test display](assets/Example.gif)

## Push Interface Documentation

Ableton has released the documentation that enabled the creation of
this library as its own project on Github,
[push-interface](https://github.com/Ableton/push-interface).

## Image Credit
## Credits

Wyang photo by [Gunawan Kartapranata](https://commons.wikimedia.org/wiki/User:Gunkarta), used under the [Creative Commons](https://en.wikipedia.org/wiki/en:Creative_Commons) [Attribution-Share Alike 3.0 Unported](https://creativecommons.org/licenses/by-sa/3.0/deed.en) license.

Wyang photo by [Gunawan Kartapranata](https://commons.wikimedia.org/wiki/User:Gunkarta), licensed under the [Creative Commons](https://en.wikipedia.org/wiki/en:Creative_Commons) [Attribution-Share Alike 3.0 Unported](https://creativecommons.org/licenses/by-sa/3.0/deed.en) license.
GifSequenceWriter by [Elliot Kroo](http://elliot.kroo.net/software/java/GifSequenceWriter/), used under the [Creative Commons](https://en.wikipedia.org/wiki/en:Creative_Commons) [Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/).

## License

Expand Down
Binary file added assets/Example.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion pom.xml
Expand Up @@ -4,7 +4,7 @@

<groupId>org.deepsymmetry</groupId>
<artifactId>wayang</artifactId>
<version>0.1.4-SNAPSHOT</version>
<version>0.1.4</version>
<packaging>jar</packaging>

<name>wayang</name>
Expand Down
236 changes: 236 additions & 0 deletions src/main/java/org/deepsymmetry/GifSequenceWriter.java
@@ -0,0 +1,236 @@
package org.deepsymmetry;

import javax.imageio.*;
import javax.imageio.metadata.*;
import javax.imageio.stream.*;
import java.awt.image.*;
import java.io.*;
import java.util.Iterator;

/**
* This class will generate an animated GIF from a sequence of individual images. Embedded in Wayang
* to facilitate capturing animations and interfaces rendered on the Push, for producing great online
* documentation.
*
* Originally created by Elliot Kroo on 2009-04-25. See http://elliot.kroo.net/software/java/GifSequenceWriter/
* James Elliott split the constructor into a variety of different versions, to accommodate the needs of Wayang.
*
* 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/
* or send a letter to Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.
*
* @author Elliot Kroo (elliot[at]kroo[dot]net)
*/
public class GifSequenceWriter {
protected ImageWriter gifWriter;
protected ImageWriteParam imageWriteParam;
protected IIOMetadata imageMetaData;


/**
* Creates a new GifSequenceWriter from an existing buffered image.
*
* @param outputStream the ImageOutputStream to be written to
* @param image the source image that will be written to the output
* @param timeBetweenFramesMS the time between frames in miliseconds
* @param loopContinuously wether the gif should loop repeatedly
* @throws IIOException if no gif ImageWriters are found
*
* @author James Elliott
*/
public GifSequenceWriter(
ImageOutputStream outputStream,
RenderedImage image,
int timeBetweenFramesMS,
boolean loopContinuously) throws IIOException, IOException {
this(outputStream, ImageTypeSpecifier.createFromRenderedImage(image), timeBetweenFramesMS,
loopContinuously);
}

/**
* Creates a new GifSequenceWriter
*
* @param outputStream the ImageOutputStream to be written to
* @param imageType one of the imageTypes specified in BufferedImage
* @param timeBetweenFramesMS the time between frames in miliseconds
* @param loopContinuously wether the gif should loop repeatedly
* @throws IIOException if no gif ImageWriters are found
*
* @author Elliot Kroo (elliot[at]kroo[dot]net)
*/
public GifSequenceWriter(
ImageOutputStream outputStream,
int imageType,
int timeBetweenFramesMS,
boolean loopContinuously) throws IIOException, IOException {
this(outputStream, ImageTypeSpecifier.createFromBufferedImageType(imageType), timeBetweenFramesMS,
loopContinuously);

}

/**
* Creates a new GifSequenceWriter
*
* @param outputStream the ImageOutputStream to be written to
* @param imageTypeSpecifier the type of images to be written
* @param timeBetweenFramesMS the time between frames in miliseconds
* @param loopContinuously wether the gif should loop repeatedly
* @throws IIOException if no gif ImageWriters are found
*
* @author Elliot Kroo (elliot[at]kroo[dot]net)
*/
public GifSequenceWriter(
ImageOutputStream outputStream,
ImageTypeSpecifier imageTypeSpecifier,
int timeBetweenFramesMS,
boolean loopContinuously) throws IIOException, IOException {

// my method to create a writer
gifWriter = getWriter();
imageWriteParam = gifWriter.getDefaultWriteParam();

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",
Integer.toString(timeBetweenFramesMS / 10));
graphicsControlExtensionNode.setAttribute(
"transparentColorIndex",
"0");

IIOMetadataNode commentsNode = getNode(root, "CommentExtensions");
commentsNode.setAttribute("CommentExtension", "Created by MAH");

IIOMetadataNode appEntensionsNode = getNode(
root,
"ApplicationExtensions");

IIOMetadataNode child = new IIOMetadataNode("ApplicationExtension");

child.setAttribute("applicationID", "NETSCAPE");
child.setAttribute("authenticationCode", "2.0");

int loop = loopContinuously ? 0 : 1;

child.setUserObject(new byte[]{ 0x1, (byte) (loop & 0xFF), (byte)
((loop >> 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<ImageWriter> 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 <tt>IIOMetadataNode</tt> 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 nNodes = rootNode.getLength();
for (int i = 0; i < nNodes; i++) {
if (rootNode.item(i).getNodeName().compareToIgnoreCase(nodeName)
== 0) {
return((IIOMetadataNode) rootNode.item(i));
}
}
IIOMetadataNode node = new IIOMetadataNode(nodeName);
rootNode.appendChild(node);
return(node);
}

/**
public GifSequenceWriter(
BufferedOutputStream outputStream,
int imageType,
int timeBetweenFramesMS,
boolean loopContinuously) {
*/

public static void main(String[] args) throws Exception {
if (args.length > 1) {
// grab the output image type from the first image in the sequence
BufferedImage firstImage = ImageIO.read(new File(args[0]));

// create a new BufferedOutputStream with the last argument
ImageOutputStream output =
new FileImageOutputStream(new File(args[args.length - 1]));

// create a gif sequence with the type of the first image, 1 second
// between frames, which loops continuously
GifSequenceWriter writer =
new GifSequenceWriter(output, firstImage.getType(), 1, false);

// write out the first image to our sequence...
writer.writeToSequence(firstImage);
for(int i=1; i<args.length-1; i++) {
BufferedImage nextImage = ImageIO.read(new File(args[i]));
writer.writeToSequence(nextImage);
}

writer.close();
output.close();
} else {
System.out.println(
"Usage: java GifSequenceWriter [list of gif files] [output file]");
}
}
}

0 comments on commit 52ac537

Please sign in to comment.