(Continuation)
The SHOW button opens a dialog in which you can specify how the captured/loaded images are displayed. To do this, it instantiates the object Display (Display.java), which displays the images in an animated GIF manner. This means that the image sequence repeats itself until you close the display window. The Display object is itself a full-fledged JFX class with its own Stage and Scene. The self-explanatory layout is JFX with Button, HBox and VBox. Therefore, there is no need to go into details.
public class Display {
/**
Display a BufferedImage array
@param images ArrayList of buffered images
@param tFrame int, the time frame between images
*/
public Display(List<BufferedImage> images, int tFrame) {
if (images.size() == 0) {
Tools.dialog(null, false, "List of BufferedImages is empty.");
return;
}
this.tFrame = tFrame;
this.images = images;
//
popup = new Stage();
popup.setOnCloseRequest(ev -> {
closed = true;
popup.close();
});
popup.setTitle("Playing Images");
BufferedImage img = images.get(0);
height = img.getHeight();
width = img.getWidth();
//
Canvas canvas = new Canvas(width, height);
gc = canvas.getGraphicsContext2D( );
gc.setFontSmoothingType(FontSmoothingType.GRAY);
//
Button next = Tools.getButton("NEXT", "Next Image");
next.setOnAction(a -> {
toggle = true;
nxt = true;
step = 1;
});
next.setDisable(true);
//
Button prev = Tools.getButton("PREV", "Previous Image");
prev.setOnAction(a -> {
toggle = true;
nxt = true;
step = -1;
});
prev.setDisable(true);
//
Button act = Tools.getButton("STOP", "Stop/Start playing");
act.setOnAction(a -> {
stop = !stop;
step = 0;
if (stop) {
next.setDisable(false);
prev.setDisable(false);
act.setText("START");
toggle = false;
} else {
next.setDisable(true);
prev.setDisable(true);
act.setText("STOP");
toggle = true;
nxt = true;
}
});
HBox hbox = new HBox(5);
hbox.setAlignment(Pos.CENTER);
hbox.setPadding(new Insets(5, 5, 5, 5));
hbox.getChildren().addAll(prev, act, next);
//
VBox vbox = new VBox(5);
vbox.setAlignment(Pos.CENTER);
vbox.setPadding(new Insets(5, 5, 5, 5));
vbox.getChildren().addAll(canvas, hbox);
Scene scene = new Scene(vbox, width+10, height+60);
scene.getStylesheets().add("gif.css");
popup.setScene(scene);
popup.show();
// start playing the images in ForkJoinPool
ForkJoinPool.commonPool().execute(() -> {
// repeat until closed is set
for (int i = 0, mx = images.size()-1; !closed; ) {
if (toggle) {
gc.drawImage(SwingFXUtils.toFXImage(images.get(i), null), 0, 0);
if (step == 0) try {
++i; // next image
TimeUnit.MILLISECONDS.sleep(tFrame);
} catch (Exception ex) { }
else { // Stepping
i += step;
nxt = false;
while (!nxt) ; // do nothing
}
} else {
gc.drawImage(SwingFXUtils.toFXImage(images.get(i), null), 0, 0);
while (!toggle) ; // do nothing
}
if (i < 0) i = mx;
else if (i > mx) i = 0;
}
});
}
//
private Stage popup;
private GraphicsContext gc;
private volatile int step = 0;
private int tFrame, height, width;
private List<BufferedImage> images;
private volatile boolean stop = false, on = false, closed = false, toggle = true, nxt = true;
}
Explanation:
The line ForkJoinPool.commonPool().execute(() -> {…}) ensures that Display executes the animation in the ForkJoinPool (concurrency/work-stealing mode). The display allows you to control the animation with 3 buttons: STOP, NEXT, PREV. The line scene.getStylesheets().add(“gif.css”) indicates that Display shares its GUI with the main application CamCorder.
- The STOP button, which changes to START when pressed and the animation stays still.
- When STOP is pressed, the NEXT and PREV buttons are enabled, allowing you to continue the animation step by step - either forward (NEXT) or backward (PREV).
-
START continues the show from where it is currently located.
GifIO is specifically designed to save BufferedImages as an animation file (.gzip or .gif) using standard Java APIs:
- GZIPInputStream
- GZIPOutputStream
- ImageReader
- ImageWriter
Since images are usually large (typically more than a few hundred KB), saving and loading the images usually take a long time. During this time, all buttons except the QUIT button are disabled. So don’t panic if this is the case. GifIO methods are static methods and can be used directly. The following statics are available:
- readGIF: Read images from a file with the suffix .gif.
- readGZIP: Read images from a file with the suffix .gzip.
- writeGIF: Write an ArrayList of BufferedImages to a file with the suffix .gif.
- writeGZIP: Write an ArrayList of BufferedImages to a file with .gzip suffix.
public class GifIO {
/**
readGIF from a gif file
@param fgif String, GIF file name
@return ArrayList of BufferedImage
@throws Exception if something is wrong
*/
public static ArrayList<BufferedImage> readGIF(String fgif) throws Exception {
ImageReader reader = (ImageReader)ImageIO.getImageReadersByFormatName("gif").next();
reader.setInput(ImageIO.createImageInputStream(new File(fgif)), false);
ArrayList<BufferedImage> list = new ArrayList<>();
ByteArrayOutputStream bao = new ByteArrayOutputStream();
BufferedImage master = null;
for (int i = 0, max = reader.getNumImages(true); i < max; ++i) {
BufferedImage image = reader.read(i);
NodeList children = reader.
getImageMetadata(i).
getAsTree("javax_imageio_gif_image_1.0").
getChildNodes();
for (int j = 0, mx = children.getLength(); j < mx; ++j) {
Node nodeItem = children.item(j);
if(nodeItem.getNodeName().equals("ImageDescriptor")) {
if(i == 0) {
master = new BufferedImage(Integer.valueOf(nodeItem.
getAttributes().
getNamedItem("imageWidth").
getNodeValue()
),
Integer.valueOf(nodeItem.
getAttributes().
getNamedItem("imageHeight").
getNodeValue()
),
BufferedImage.TYPE_INT_ARGB
);
}
master.getGraphics().drawImage(image,
Integer.valueOf(nodeItem.
getAttributes().
getNamedItem("imageLeftPosition").
getNodeValue()
),
Integer.valueOf(nodeItem.
getAttributes().
getNamedItem("imageTopPosition").
getNodeValue()
),
null
);
ImageIO.write(master, "PNG", bao);
list.add(ImageIO.read(new ByteArrayInputStream(bao.toByteArray())));
bao.reset();
}
}
}
return list;
}
/**
readGZIP BufferedImage from a gzip file
@param fgzip String, GIF file name
@return ArrayList of BufferedImage
@throws Exception if something is wrong
*/
public static ArrayList<BufferedImage> readGZIP(String fgzip) throws Exception {
byte[] buf = java.nio.file.Files.readAllBytes((new File(fgzip)).toPath());
if (buf.length == 0) throw new Exception(fgzip+" is empty");
ByteArrayInputStream bis = new ByteArrayInputStream(buf);
GZIPInputStream gi = new GZIPInputStream(bis);
//
buf = new byte[1048576]; // 1MB
ArrayList<BufferedImage> bImgLst = new ArrayList<>();
ByteArrayOutputStream bao = new ByteArrayOutputStream();
for (int n = gi.read(buf); n > 0; n = gi.read(buf)) bao.write(buf, 0, n);
bao.flush();
buf = bao.toByteArray();
bao.close();
gi.close();
//
for (int i = 0, l; i < buf.length; i += (4+l)) {
l = (int)((buf[i]&0xFF)<<24)+(int)((buf[i+1]&0xFF)<<16)+
(int)((buf[i+2]&0xFF)<<8)+(int)(buf[i+3]&0xFF);
bImgLst.add(ImageIO.read(new ByteArrayInputStream(buf, i+4, l)));
}
return bImgLst;
}
/**
@param zR float, Zooming Ratio between 2.0 .. 0.1 (max. 2.0, min. 0.1)
@param imgLst ArrayList of BufferedImages
@param outFile String, the outputfile.gif
@param loop , 0: loop repeatedly, 1: no loop
@throws Exception if something is wrong
*/
public static void writeGIF(float zR, ArrayList<BufferedImage> imgLst,
String outFile, int loop) throws Exception {
BufferedImage img = imgLst.get(0);
ImageOutputStream output = new FileImageOutputStream(new File(outFile));
zR = zR < 2.0f? zR < 0.1f? 0.1f:zR : 2.0f;
GifIO writer = new GifIO(output, loop);
if (zR > 0.95f && zR < 1.05f) {
for(BufferedImage bImg : imgLst) writer.writeToSequence(bImg);
} else {
int type = img.getType();
int wi = (int)(Math.ceil(zR * img.getWidth()));
int hi = (int)(Math.ceil(zR * img.getHeight()));
for(BufferedImage bImg : imgLst) {
BufferedImage image = new BufferedImage(wi, hi, type);
Graphics2D graphics2D = image.createGraphics();
graphics2D.setBackground(Color.WHITE);
graphics2D.setPaint(Color.WHITE);
graphics2D.fillRect(0, 0, wi, hi);
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.drawImage(bImg, 0, 0, wi, hi, null);
writer.writeToSequence(image);
}
}
output.flush();
writer.close();
output.close();
}
/**
@param zR float, Zooming Ratio between 2.0 .. 0.1 (max. 2.0, min. 0.1)
@param imgLst ArrayList of BufferedImages
@param outFile String, the outputfile.gzip
@throws Exception if something is wrong
*/
public static void writeGZIP(float zR, ArrayList<BufferedImage> imgLst,
String outFile) throws Exception {
GZIPOutputStream go = new GZIPOutputStream(new FileOutputStream(outFile, false), true);
ByteArrayOutputStream bos = new ByteArrayOutputStream( );
ByteArrayOutputStream bao = new ByteArrayOutputStream( );
zR = zR < 2.0f? zR < 0.1f? 0.1f:zR : 2.0f;
if (zR < 0.95f || zR > 1.05f) {
for(BufferedImage bImg : imgLst) write(bImg, bao, bos);
} else {
BufferedImage img = imgLst.get(0);
int wi = (int)(Math.ceil(zR * img.getWidth()));
int hi = (int)(Math.ceil(zR * img.getHeight()));
for(BufferedImage bImg : imgLst) {
img = new BufferedImage(wi, hi, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics2D = img.createGraphics();
graphics2D.setBackground(Color.WHITE);
graphics2D.setPaint(Color.WHITE);
graphics2D.fillRect(0, 0, wi, hi);
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.drawImage(bImg, 0, 0, wi, hi, null);
write(img, bao, bos);
}
}
go.write(bos.toByteArray());
bos.close();
go.flush( );
go.close( );
}
/**
write a BufferedImage in GZIP format
* @param img BufferedImage
* @param bao ByteArrayOutputStream, the on-behalf stream
* @throws Exception if something is wrong
*/
private static void write(BufferedImage img,
ByteArrayOutputStream bao,
ByteArrayOutputStream bos) throws Exception {
ImageIO.write(img, "JPG", bao);
int le = bao.size(); // get the Image size
bos.write(new byte[] {(byte)((le >> 24)&0xFF), (byte)((le >> 16)&0xFF),
(byte)((le >> 8)&0xFF), (byte)( le & 0xFF)
}
);
bos.write(bao.toByteArray());
bos.flush();
bao.reset();
}
/**
* private Constructor
*
* @param outStream the ImageOutputStream to be written to
* @param loop int, 0: loop repeatedly, 1: no loop
* @throws OException if something is wrong
*/
private GifIO(ImageOutputStream outStream, int loop) throws Exception {
Iterator<ImageWriter> iter = ImageIO.getImageWritersBySuffix("gif");
if(!iter.hasNext()) throw new IIOException("No GIF Image Writers Exist");
imgWriter = iter.next();
//
imgWriteParam = imgWriter.getDefaultWriteParam();
ImageTypeSpecifier imageTypeSpecifier = ImageTypeSpecifier.
createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB);
imgMetaData = imgWriter.getDefaultImageMetadata(imageTypeSpecifier, imgWriteParam);
String metaFormatName = imgMetaData.getNativeMetadataFormatName();
IIOMetadataNode root = (IIOMetadataNode) imgMetaData.getAsTree(metaFormatName);
IIOMetadataNode graphicsControlExtensionNode = getNode(root, "GraphicControlExtension");
graphicsControlExtensionNode.setAttribute("disposalMethod", "none");
graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE");
graphicsControlExtensionNode.setAttribute("transparentColorFlag", "FALSE");
graphicsControlExtensionNode.setAttribute("delayTime", "10"); // 10 mSecond
graphicsControlExtensionNode.setAttribute("transparentColorIndex", "0");
IIOMetadataNode commentsNode = getNode(root, "CommentExtensions");
commentsNode.setAttribute("CommentExtension", "Created by JAVA");
IIOMetadataNode appEntensionsNode = getNode(root, "ApplicationExtensions");
IIOMetadataNode child = new IIOMetadataNode("ApplicationExtension");
// don't change applicationID "NETSCAPE"
child.setAttribute("applicationID", "NETSCAPE");
child.setAttribute("authenticationCode", "2.0");
// loop: accept only 0 or 1
child.setUserObject(new byte[]{ 0x01, (byte)(loop & 0x01), 0x00 });
appEntensionsNode.appendChild(child);
imgMetaData.setFromTree(metaFormatName, root);
imgWriter.setOutput(outStream);
imgWriter.prepareWriteSequence(null);
}
//
private void writeToSequence(RenderedImage img) throws IOException {
imgWriter.writeToSequence(new IIOImage(img, null, imgMetaData),imgWriteParam);
}
/**
* Close this GifIO object. This does not close the underlying
* stream, just finishes off the GIF.
* @throws OException if something is wrong
*/
private void close() throws IOException {
imgWriter.endWriteSequence();
}
/**
* 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);
}
//
private ImageWriter imgWriter;
private IIOMetadata imgMetaData;
private ImageWriteParam imgWriteParam;
}
Explanation:
GifIO has a private constructor that allows writeGIF to instantiate the required ImageIO components within the static Method. The line GifIOwriter = new GifIO(output, loop) is this instantiation. For more details about the APIs used from the javax.imageio package, see Oracle’s Java website.
To beautify the appearance of the GUI, JAVA JFX allows the inclusion of CSS . CSS stands for Cascading Style Sheets. The Oracle CSS reference is a bit awkward. However, if you are well versed in CSS and JFX, you will not have any difficulty creating a CSS file. The rules are relatively simple. Example: A CheckBox or ComboBox looks like this in CSS:
CSS:
.check-box {
-fx-font-size:12px;
-fx-font-weight: bold;
-fx-text-fill:#333333;
}
// or ComboBox
.combo-box .cell {
-fx-text-fill: blue;
-fx-font-size: 12px;
-fx-font-weight: bold;
}
Explanation: The dot indicates the actual JavaFX element used. In many cases, the name of the JavaFX element can be used directly, e.g. .textfield for TextField or .textarea for TextArea. If you’ve tried it and it doesn’t work, you can experiment with a hyphen (-) like in CheckBox or ComboBox. And that’s the trick when working with JavaFX CSS.
If you are running JavaFX with FXML, you can reference the fxml name with a leading hash (#) like this:
<Button fx:id="save" onAction="#saveIt" text="SAVE"
prefHeight="40.0" prefWidth="120.0" />
and the reference to this SAVE button (here fx:id=“save”) in the CSS file is #save:
#save {
-fx-background-image: url("save.png"); /* insert icon */
}
Download gif_source.zip
Joe