Virtual Camcorder in JavaFX

Virtual Camcorder in JavaFX

A few words about me: I belong to Gen X who developed complex software using a simple editor (e.g. notepad++ or jedit, etc.). I was the senior developer of BEA Weblogic (which is now owned by Oracle). In 2001 I sold my BEA shares and retired at exactly 50. Today, as an old man, I still develop some apps for my own needs and… just for fun. Unlike Gen-Y and Gen-Z, we have always tried to keep software development as simple as possible. No unnecessary dependency files, no framework, no IDE. Everything is manageable and controllable. It’s simple because the more files you have to manage, the more confusion there is for yourself and within the team. And in the end, no one can see through or master a huge pile of different files like XMLs, resources of icons and images, text files, sources, etc.

I believe everyone knows animated image with the suffix GIF (or Graphics Interchange Format). As an IT developer, have you ever thought about how you can develop an app that captures videos and converts the images into a GIF file? This is very easy in JAVA. I’ll show you how to create an app that captures videos (e.g. YouTube or any animated GIF) on the screen and lets you create your own GIF files that can be edited, etc.

First of all, an animation is a sequence of images that are slightly different from each other and occur in a rapid timeframe. Normally the animation is vivid when the sequence of timeframes is around 25 frames per second. There are two ways to capture images on the screen: an external camera or a screenshot. The first option is out of the question. The second option basically simulates an external camera that can take the screenshots fast enough to display a vivid animation (i.e. 25 time frames per second).

Then some words about coding: Coding is a question of belief. There is someone who relies only on framework and IDE which produce a heap of dependency files and lots of small APIs grouped into “package”. The others, for example, love simple but convenient editors like Notepad++ or Jedit. Whatever the people like, coding is similar to construction of a house: first the foundation, then the house frame, then the roof, then the door and windows, etc. None of the works can be done in parallel. For this reason, I will show you the codes and basic development of an application without going into Frameworks or IDEs.
In addition, I will show you how to “exploit” all the most beautiful JavaFX features that are hidden or unknown to many. For example, JavaFX with multiple Stages (like Swing with multiple JFrames) always floats on top (regardless of other running apps), JavaFX CSS tricks and last but not least the use of a concurrency pool (ForkJoinPool).
Note: JavaFX is not an integral part of Java as of JDK 11, so I recommend you use the most stable JDK 8 or JDK 9.

The basic development environment:

  1. Working directory: for example: C:\myapp\gif (similar in Linux), where the sources are created.
  2. Compile: Open a CMD window (terminal under Linux). Example:
    cmd-windows
  3. Set CLASSPATH. For example, on WINDOWS: Create a batch file setCP.bat as follows:
// on Windows - should be in System Environment variables
set CLASSPATH=%CLASSPATH%;.;.\classes
// on LINUX - should be in .profile
export CLASSPATH=$CLASSPATH:.:./classes

The 3rd step includes the current (a dot) working directory and the subdirectory “classes” (.\classes or ./classes) to the Java variable CLASSPATH.

The source that simulates a camera is CamCorder.java (Camera + reCorder) and is written in JavaFX. As mentioned above, we lay the foundation for CamCorder with the control panel, then the camera frame is the “lens”. From them we begin to develop the functionalities of a virtual camera. The source is written in JAVA JFX. At the end of this tutorial, I’ll post the complete package of CamCorder here in ZIP format.

The CamCorder Control Panel (CamCorder.java)

public class CamCorder extends Application {
  //
  public void start(Stage panel) {
    this.panel = panel;
    panel.setOnCloseRequest(ev -> {
      ev.consume(); // Ignore the close by pressing X in the rightmost upper corner
    });
    ...
    focus = Tools.getButton("FOCUS", "Focus Screen Image");
    focus.setOnAction(ev -> {
    });
    start = Tools.getButton("START", "Start Recording Screen");
    start.setOnAction(ev -> {
      if (!focused) {
        System.out.println("Nowhere on SCREEN is focused.");
        return;
      }
      if (idle) {
        start.setText("STOP");
      } else {
        start.setText("START");
      }
    });
    //
    load = Tools.getButton("LOAD", "Load from GIF/GZIP image file");
    load.setOnAction(ev -> {
    });
    edit = Tools.getButton("EDIT", "Edit BufferedImages");
    edit.setOnAction(ev -> {
      editing();
    });
    undo = Tools.getButton("UNDO", "Undo Editing");
    undo.setOnAction(ev -> {
    });
    show = Tools.getButton("SHOW", "Show animated BufferedImages");
    show.setOnAction(ev -> {
    });
    save = Tools.getButton(new Image(CamCorder.class.getResourceAsStream("save.png")),
                                 "Save to GIF/GZIP file");
    save.setOnAction(ev -> {
    });
    reset = Tools.getButton("RESET", "RESET");
    reset.setOnAction(ev -> {
    });
    Button quit = Tools.getButton("QUIT", "Quit & Exit CamCorder");
    quit.setOnAction(ev -> {
      panel.close();
      Platform.exit();
      System.exit(0);
    });
    VBox bBox = new VBox(5);
    bBox.setAlignment(Pos.CENTER);
    // Insets(top, right, bottom, left)
    bBox.setPadding(new Insets(10, 0, 15, 0));
    bBox.getChildren().addAll(focus, start, load, edit, undo, show, save, reset, quit);
    Scene scene = new Scene(bBox, 145, 350);
    scene.getStylesheets().add("gif.css");
    panel.getIcons().add(new Image(CamCorder.class.getResourceAsStream("sanduhr.gif")));
    panel.setAlwaysOnTop(true);
    panel.setResizable(false);
    panel.setScene(scene);
    panel.setY(0);
    panel.setX(0);
    panel.show();

CamCorder ControlPanel
Camcorder_ControlPanel
Explanation:

  • All JFX buttons are activated by lambda expression (e -> { … });
  • Line scene.getStylesheets().add(“gif.css”) uses the style from the external file gif.css
  • Line panel.setAlwaysOnTop(true) always places the ControlPanel above other applications.

The ControlPanel is the foundation and contains 9 buttons. With the implementation of Tools.getButton(), a hint text with the description of the button appears when the mouse pointer hovers over the buttons. Nine buttons means 9 CamCorder activities need to be implemented.

The Tools codes (Tools.java):

public class Tools {
  ...
  /**
   @param icon JFX Image or String
   @param txt String as tooltip or button name (if icon = null)
   @return Button
  */
  public static Button getButton(Object icon, String txt) {
    Button button = null;
    if (icon == null || icon instanceof String) {
      button = new Button(icon == null? txt:(String)icon);
      // direct CSS style
      button.setStyle("-fx-font-size:11; -fx-font-weight: bold;");
    } else try {
      button = new Button( );
      button.setGraphic(new ImageView((Image) icon));
    } catch (Exception ex) {
      button = new Button(txt);
      button.setStyle("-fx-font-weight: bold;");
    }
    Tooltip tt = new Tooltip();
    tt.setText(txt);
    tt.setStyle("-fx-base: #AE3522; -fx-text-fill: orange; -fx-font-cb: bold;");
    button.setTooltip(tt);
    return button;
  }

Explanation:

  • If the icon is null or a string, the button name is either txt (null) or this icon string
  • otherwise the button is represented by the specified icon.

(to be continued)

2 Likes

(Continuation)

You may ask why I designed a ControlPanel directly and not with MVC (Model-View-Control)? Well, MVC with JavaFX is common, but not a must. It is a “nice-to-have” model that can become complex, unclear and confusing when the functionalities are closely intertwined. Whatever MVC means and works, the development steps are the same:

  • the foundation: the model,
  • the frame: the view or appearance,
  • the realization: the implementation of each component of the view (control).

In this case, the view here is the ControlPanel, which combines the model and the view (as fxml file). More views means more FXML files. And this is the main problem of dependency files (fxml) from the view, when the number of dependency files becomes too many and too confusing.
With MVC model:

Model:

public class Camcorder_Model extends Application {
  public void start(Stage stage) throws Exception {
    stage.setTitle("Camcorder");
    FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml));
    VBox root = (VBox)loader.load();
    final Camcorder_Controller camcorder = loader.<Camcorder_Controller>getController();
    stage.setScene(new Scene(root));
    stage.show();
  }
  // invoked by JVM
  public void stop() {
    Platform.exit();
    System.exit(0);
  }
}

View in fxml

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.*?>
...
<VBox id="VBox" fx:id="root" alignment="center" spacing="5" 
    maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
    prefHeight="350.0" prefWidth="145.0" xmlns:fx="https://joe.com/fxml"
    fx:controller="Camcorder_Controller">
    <padding><Insets top="10" right="0" bottom="15" left="0"/></padding>
       <children>  
        <Button fx:id="focus" onAction="#actionFocus"  text="FOCUS" />
        <Button fx:id="start" onAction="#actionStart" text="START" />
        <Button fx:id="load" onAction="#actionLoad" text="LOAD" />
        <Button fx:id="edit" onAction="#actionEdit" text="EDIT" />
        <Button fx:id="undo" onAction="#actionUndo" text="UNDO" />
        <Button fx:id="show" onAction="#actionShow" text="SHOW" />
        <Button fx:id="save" onAction="#actionSave" />
        <Button fx:id="reset" onAction="#actionReset" text="RESET" />
        <Button fx:id="quit" onAction="#actionQuit" text="QUIT" />
       </children>
      </VBox>
   </children>
   <stylesheets>
       <URL value="@joe.css" />
   </stylesheets>
</VBox>

Controller for Camcorder could be as following:

public class Camcorder_Controller implements Initializable {
    // a mandatory implementation
    public void initialize(URL location, ResourceBundle resources) {
        ...
    }
    @FXML Button focus, start, load, edit, undo, show, save, reset;
    
    @FXML public void actionFocus( ) {
        ...
    }
    @FXML public void actionStart() {
        ...
    }
    @FXML public void actionLoad() {
        ...
    }
    ...
}

Let’s go back to our original purpose: simplicity and clarity with fewer independence files as possible. In the previous section I showed you the ControlPanel, which is the foundation and contains 9 buttons. With the implementation of Tools.getButton(), a hint text with the description of the button appears when the mouse pointer hovers over the buttons. Nine buttons means 9 CamCorder activities need to be implemented. The foundation stone has been laid. Today I’ll show you the CamCorder frame, which is the “lens” of the virtual camera.

How it works?
The camera lens is used to capture a scene. A mechanism controls the incidence of light on a specific area, making the contours of objects in that area sharper and clearer. It’s called “focusing.” If the cameraman is happy with it, pictures can be taken.
The lens here is nothing more than a semi-transparent JFX screen that covers the entire screen and enables you to observe the actual scene on the screen, allowing you to focus on the specific area. The control mechanism is the FOCUS button in conjunction with the movement of the mouse cursor to create a focusing rectangular area: the 1st mouse click defines the upper right corner and the 2nd mouse click marks the lower left corner. The area within this rectangle is now in focus.
By clicking the START button (the text of which changes to STOP), you begin recording the events within the focused area. With the next click on STOP (the text changes back to START) you end the recording process.

public class CamCorder extends Application {
  //
  public void start(Stage panel) {
    ...
    // Create a lens for CamCorder
    try { // take a screen shot to get the screen size
      BufferedImage screen = (new java.awt.Robot()).
        createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()));
      wMx = screen.getWidth(); // max Window width
      hMx = screen.getHeight();// max Window height
    } catch (Exception ex) {
      wMx = 1366; hMx = 768;
    }
    // create virtual lens: JFX canvas of screen size
    Canvas canvas = new Canvas(wMx, hMx);
    // create the 2nd JFX Stage for Canvas
    Stage stage = new Stage();
    Pane pane = new Pane();
    pane.getChildren().addAll(canvas);
    // hMx+13: adjust to the FrameBar with Iconify/Enlarge/Close button
    Scene window = new Scene(pane, wMx, hMx+13);
    window.setFill(Color.TRANSPARENT); // transparent window scene
    window.getStylesheets().add("gif.css");
    stage.setScene(window);
    // set semi-opacity (semi-transparent)
    stage.setOpacity(0.5);
    //
    flag = false;
    move = false;
    focused = false;
    gc = canvas.getGraphicsContext2D( );
    gc.setFontSmoothingType(FontSmoothingType.GRAY);
    canvas.addEventFilter(MouseEvent.ANY, (e) -> canvas.requestFocus());
    // Focussing with the mouse pointer
    canvas.setOnMouseMoved(e -> {
      if (!move || !flag) return;
      int xx = (int)e.getX();
      int yy = (int)e.getY();
      //drawing focusing rectangle....
      gc.clearRect(0, 0, wMx, hMx);
      gc.setLineWidth(2);
      gc.setStroke(Color.RED);
      gc.strokeLine(x0, y0, xx, y0);
      gc.strokeLine(x0, yy, xx, yy);
      gc.strokeLine(x0, y0, x0, yy);
      gc.strokeLine(xx, y0, xx, yy);
    });
    canvas.setOnMouseReleased(e -> {
      if (!flag) return;
      int xx = (int)e.getX();
      int yy = (int)e.getY();
      // 1st Click: upper coordinate
      if (x0 < 0) {
        x0 = xx;
        y0 = yy;
        move = true;
        return;
      }
      // 2nd Click: lower coordinate
      move = false;
      flag = false;
      // setting focusing area: x0, y0, width and height
      if (x0 > xx) {
        int x = x0;
        x0 = xx;
        xx = x;
      }
      if (y0 > yy) {
        int y = y0;
        y0 = yy;
        yy = y;
      }
      // the size of focussing area
      width = xx-x0;
      height = yy-y0;
      // wait 200 ms before disappearing into background
      waiting(200);
      stage.hide();
      disable(false);
      focused = true;
    });
    ...
    focus = Tools.getButton("FOCUS", "Focus Screen Image");
    focus.setOnAction(ev -> {
      gc.clearRect(0, 0, wMx, hMx);
      disable(true); // disable all buttons
      stage.show();  // show the canvas in semi-transparent
      x0 = y0 = -1;  // reset first coordinate XY
      flag = true;   // set Flag telling FOCUSING is ready
    });
    start = Tools.getButton("START", "Start Recording Screen");
    start.setOnAction(ev -> {
      if (!focused) {
        Tools.dialog(panel, true, "Nowhere on SCREEN is focused.");
        return;
      }
      if (idle) { // START
        disable(true);
        // except START/STOP
        start.setDisable(false);
        start.setText("STOP"); // change text to STOP
        bImgList.clear(); // clear Image List
        saveList.clear(); // clear Save List
        idle = false;     // start ScreenShot
        done = false;     //
      } else { // End of taking images
        start.setText("START");
        disable(false);
        idle = true;
        done = true;
      }
    });
    ...
    // create virtual Camera recording mechanism
    ForkJoinPool.commonPool().execute(() -> {
      BufferedImage bImg, image;
      while (running) {
        while (idle)
          waiting(100);
        while (!done) {
          try { // take screenshot
            bImg = (new java.awt.Robot()).
                   createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()));
            image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
            // crop the focused area
            for (int y = y0, b = 0; y < hMx && b < height; ++y, ++b)
              for (int x = x0, a = 0; x < wMx && a < width; ++x, ++a)
                image.setRGB(a, b, bImg.getRGB(x,y));
            // save in Image List
            bImgList.add(image);
            TimeUnit.MILLISECONDS.sleep(20);
          } catch (Exception ex) { }
        }
      }
    });
    ...

Explanation

  • Line ForkJoinPool.commonPool().execute(() -> { … }); starts a background snapshot creation task in ForkJoinPool. It runs until “Running” is set to “False” (“QUIT” button). It stands idle as long as idle is true (see START/STOP button).
  • Line bImg = (new java.awt.Robot()).createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())); is the action to capture images.
  • Line image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); creates an image with the focused size.
  • for (int y = y0, b = 0; y < hMx && b < height; ++y, ++b)
      for (int x = x0, a = 0; x < wMx && a < width; ++x, ++a)
          image.setRGB(a, b, bImg.getRGB(x,y));

copies the focused screenshot area into the image.

(to be continued)

(Continuation)

I showed you how to implement the virtual “camera lens” and its focusing mechanism. The focusing rectangular area is created using the FOCUS button in conjunction with the mouse pointer (coordinates at the top left/bottom right by clicking the left mouse button). The recording process is carried out by a background task running in JVM ForkJoinPool and controlled via the START/STOP button. Today I’ll briefly talk about the rest of the control panel buttons. The codes are straightforward with comments. You may notice that some methods are called with the prefix GifIO.xxxGIF or GifIO.xxxGZIP or are initiated as new Display(). They are part of the CamCorder package and the CSS file “gif.css”. And I will explain them later in this tutorial.

LOAD button: Loads an existing GIF/GZIP file for editing or review.

    load = Tools.getButton("LOAD", "Load from GIF/GZIP image file");
    load.setOnAction(ev -> {
      disable(true);
      fName = Tools.getFile(panel, dir);
      if (fName != null) try {
        if (fName.toLowerCase().lastIndexOf(".gif") > 0) {
          bImgList = GzipWriter.extractGIF(fName);
        } else {
          bImgList = GifWriter.extractGZIP(fName);
        }
        focused = false;
      } catch (Exception ex) {
        Tools.dialog(panel, false, ex.toString());
      }
      disable(false);
    });

EDIT button: Allows you to edit the GIF/GZIP file.
UNDO button: Undoes the edited GIF/GZIP file.

    edit = Tools.getButton("EDIT", "Edit BufferedImages");
    edit.setOnAction(ev -> {
      editing();
    });
    undo = Tools.getButton("UNDO", "Undo Editing");
    undo.setOnAction(ev -> {
      if (!saveList.isEmpty()) {
        disable(true);
        bImgList.clear(); // remove the content
        for (BufferedImage img:saveList) bImgList.add(Tools.iClone(img));
        disable(false);
      }
      Tools.dialog(panel, true, "Original BufferedImage List is restored");
    });
    ...
  // this is an editing dialog
  private void editing() {
    int mx = bImgList.size();
    disable(true);
    if (mx > 0) try { // NOT empty
      BufferedImage img = bImgList.get(0);
      int x = img.getWidth();
      int y = img.getHeight();
      if (x < 100 || y < 100) {
        Tools.dialog(panel, false, "Images are too small for Editing. Min. 100x100.");
        disable(false);
        return;
      }
      if (saveList.isEmpty()) // clone the original
        for (BufferedImage bimg:bImgList) saveList.add(Tools.iClone(bimg));
      //
      Dialog<ButtonType> dia = new Dialog<>();
      dia.setTitle("CamCorder");
      dia.setHeaderText("Edit GIF/GZIP Images");
      DialogPane dp = dia.getDialogPane();
      dp.getStylesheets().add("gif.css");
      dp.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
      TextField tfa = new TextField("Any Text");
      tfa.setPrefWidth(245);
      HBox h1 = new HBox(5);
      h1.setAlignment(Pos.CENTER_LEFT);
      h1.getChildren().addAll(new Label("Your Text    "), tfa);
      //
      TextField tff = Tools.getTextField(0);
      TextField tfe = Tools.getTextField(mx);
      TextField tfo = Tools.getTextField(15);
      HBox h2 = new HBox(5);
      h2.setAlignment(Pos.CENTER_LEFT);
      h2.getChildren().addAll(new Label("From Image"), tff,
                              new Label("To Image"), tfe,
                              new Label("Font Size"), tfo);
      ComboBox<String> cbc = new ComboBox<>();
      cbc.getItems().addAll("YELLOW!WHITE!BLACK!GREEN!BLUE!RED".split("!"));
      java.awt.Color colors[] = { java.awt.Color.YELLOW,
                                  java.awt.Color.WHITE,
                                  java.awt.Color.BLACK,
                                  java.awt.Color.GREEN,
                                  java.awt.Color.BLUE,
                                  java.awt.Color.RED
                                };
      idx = 0;                   
      cbc.setValue("YELLOW");
      cbc.setOnAction(a -> {
        idx = cbc.getSelectionModel().getSelectedIndex();
      });
      ComboBox<String> cbf = new ComboBox<>();
      cbf.getItems().addAll("Arial!Courier!Georgia!Lucida!Times!Tahoma!Verdana".split("!"));
      cbf.setValue("Georgia");
      font = "Georgia";
      cbf.setOnAction(a -> {
        font = cbf.getSelectionModel().getSelectedItem();
        if ("Times".equals(font)) font = "Times New Roman";
      });
      type = 0;
      int types[] = { java.awt.Font.BOLD,
                      java.awt.Font.ITALIC,
                      java.awt.Font.PLAIN
                    };
      ComboBox<String> cbt = new ComboBox<>();
      cbt.getItems().addAll("BOLD!ITALIC!PLAIN".split("!"));
      cbt.setValue("BOLD");
      cbt.setOnAction(a -> {
        type = cbt.getSelectionModel().getSelectedIndex(); 
      });
      HBox h3 = new HBox(17);
      h3.setAlignment(Pos.CENTER_LEFT);
      h3.getChildren().addAll(cbc, cbf, cbt);
      //
      TextField tfx = Tools.getTextField(10);
      TextField tfy = Tools.getTextField(10);
      HBox h4 = new HBox(10);
      h4.setAlignment(Pos.CENTER_LEFT);
      h4.getChildren().addAll(new Label("X position (0.."+x+")"), tfx,
                              new Label("Y position (0.."+y+")"), tfy);
      VBox box = new VBox(5);
      box.getChildren().addAll(h1, h2, h4, h3);
      dp.setContent(box);
      dp.setPrefSize(400, 200);
      Tools.setPosition(panel, dia);
      if (dia.showAndWait().get() == ButtonType.CANCEL) {
        disable(false);
        return;
      }
      x = Integer.parseInt(tfx.getText().trim());
      y = Integer.parseInt(tfy.getText().trim());
      int b = Integer.parseInt(tff.getText().trim());
      int e = Integer.parseInt(tfe.getText().trim());
      if (e > mx) e = mx;
      if (b >= e) {
        Tools.dialog(panel, false, "Begin at:"+b+". End at:"+e+"?");
        return;
      }
      String text = tfa.getText().trim();
      int fsize = Integer.parseInt(tfo.getText().trim());
      if (fsize < 10) fsize = 10;
      if (fsize > 40) fsize = 40;
      if (y < fsize) y = fsize+5;
      for (int i = b; i < e; ++i) {
        java.awt.Graphics2D g =(java.awt.Graphics2D)(bImgList.get(i)).getGraphics();
        g.setFont(new java.awt.Font(font, types[type], fsize));
        g.setPaint(colors[idx]);
        g.drawString(text, x, y);
        g.dispose();
      }
    } catch (Exception ex) {
      Tools.dialog(panel, false, ex.toString());
    }
    disable(false);
  }
  // start, load, edit, save,...;
  private void disable(boolean boo) {
    focus.setDisable(boo);
    start.setDisable(boo);
    reset.setDisable(boo);
    show.setDisable(boo);
    load.setDisable(boo);
    edit.setDisable(boo);
    save.setDisable(boo);
    undo.setDisable(boo);
  }
  ...

Editing Dialog
Editing
SHOW button: Shows the captured images (with/without edited text).

    show = Tools.getButton("SHOW", "Show animated BufferedImages");
    show.setOnAction(ev -> {
      int mx = bImgList.size();
      if (mx == 0) return; // Nothing to show
      //
      Dialog<ButtonType> dia = new Dialog<>();
      dia.setTitle("CamCorder");
      dia.setHeaderText("SHOW GIF/GZIP Images");
      DialogPane dp = dia.getDialogPane();
      dp.getStylesheets().add("gif.css");
      dp.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
      //
      TextField tff = Tools.getTextField(0);
      TextField tfe = Tools.getTextField(mx);
      CheckBox cb = new CheckBox("Reverse");
      HBox hbox = new HBox(7);
      hbox.setAlignment(Pos.CENTER);
      hbox.getChildren().addAll(new Label("From Image"), tff,
                                new Label("To Image"), tfe, cb);
      dp.setContent(hbox);
      dp.setPrefSize(400, 100);
      Tools.setPosition(panel, dia);
      if (dia.showAndWait().get() == ButtonType.OK) {
        int b = Integer.parseInt(tff.getText());
        int e = Integer.parseInt(tfe.getText());
        if (b >= e) {
          Tools.dialog(panel, false, "Begin: "+b+" >= end: "+e);
          return;
        }
        ArrayList<BufferedImage> al = new ArrayList<>();
        if (cb.isSelected()) // forward or reverse ?
             for (int i = e-1; i >= b; --i) al.add(bImgList.get(i));
        else for (int i = b; i < e; ++i) al.add(bImgList.get(i));
        new Display(al, 40); // start display the captured/loaded image
      }
    });

show
SAVE button with a floppy disk icon: Saves the captured/edited images in a GIF/GZIP file.
RESET button: Resets the focussed area.
QUIT button: Quit and exit CamCorder. Before exit, virtual camera will be stopped and all stages will be closed.

    save = Tools.getButton(new Image(CamCorder.class.getResourceAsStream("save.png")),
                                 "Save to GIF/GZIP file");
    save.setOnAction(ev -> {
      saving();
    });
    reset = Tools.getButton("RESET", "RESET");
    reset.setOnAction(ev -> {
      idle = true;
      done = true;
      flag = false;
      move = false;
      x0 = y0 = -1;
      focused = false;
      saveList.clear();
      bImgList.clear();
    });
    Button quit = Tools.getButton("QUIT", "Quit & Exit CamCorder");
    quit.setOnAction(ev -> {
      done = true;
      idle = false;
      running = false; // stop Virtual Camera
      //
      waiting(10);
      // close CanvasPanel and ControlPanel
      stage,close();
      panel.close();
      Platform.exit();
      System.exit(0);
    });
    ...
  private void saving() {
    int mx = bImgList.size();
    if (mx == 0) return;
    //
    disable(true);
    // set the default name
    if (fName == null) fName = dir+File.separator+"image.gif";
    fName = Tools.getFile(panel, fName, "SAVE file as GIF/GZIP");
    if (fName == null) { // empty name
      disable(false);

As you can see, the CamCorder app is “dialog intensive”. Each CamCorder action leads to a self-explanatory dialog, while the other buttons (except EXIT) are disabled to prevent unwanted overlap of two or more CamCorder actions (like in real camcorder).
The SAVE dialog begins with a request for a file name “Working Directory\GIFimages\image.gif” on Windows (or $HOME/GIFimages/image.gif on Linux) for captured images. If the directory **GIFImages" does NOT exist, this directory will be created. When loading the images, the filename is the loaded filename. If you empty the input field and press ENTER, a FileChooser will appear where you can select any name. Either with this name or you can overwrite the name with another name to save. After clicking OK, a slider will appear to enlarge (more than 1) or reduce (less than 1) the images (Enlarge/Reduce).
save
You might be wondering why I’m talking about GZIP together with GIF, right? Let me explain:
GIF was developed by a team at online service provider CompuServe led by American computer scientist Steve Wilhite and released on June 15, 1987, while GZIP is a file format and software application for file compression and decompression. The program was created by Jean-loup Gailly and Mark Adler as a free software replacement for the compression program used in early Unix systems, and was intended for use by GNU (from which the g of gzip is derived). Version 0.1 was first publicly released on October 31, 1992, version 1.0 followed in February 1993 (sources: WIKIPEDIA). GZIP compressed image files are “smaller” than GIF images, which are compressed using the Lempel-Ziv-Welch (LZW) lossless data compression technique to reduce file size without compromising visual quality. The ratio between GIF:GZIP is approximately 8:1 and the differences between the GIF and GZIP images are so marginal that one could hardly notice them. For this purpose I developed the GZIP IO alongside the GIF IO. Both IOs will be explained in the next sessions.

Note: The GZIP image file cannot be viewed/played by browsers or other imaging standard tools - except this CamCorder.

Here is an animated piece of the VanCleef Cowboy movie
vanCleef

(to be continued)

(Continuation)

I have discussed with you in three previous sessions about the 9 activities of ControlPanel or the foundation of CamCorder. Today I will tell you about the frame or canvas that the virtual camera represents. As you learned in the 2nd session, the canvas has its own JFX Stage/Scene and is executed by a background task in ForkJoinPool. The ForkJoinPool is the extension of JAVA ExecutorService and is tailored for parallel work stealing mode (best for concurrency). The virtual camera is activated by FOCUS and then START and set to idle by STOP.

Explanation:
The "camera lens"

    Canvas canvas = new Canvas(wMx, hMx);
    saveList = new ArrayList<>();
    bImgList = new ArrayList<>();
    // Focusing window
    Stage stage = new Stage();
    Pane pane = new Pane();
    pane.getChildren().addAll(canvas);
    // hMx+13: adjust to the FrameBar with Iconify/Enlarge/Close button
    Scene window = new Scene(pane, wMx, hMx+13);
    window.setFill(Color.TRANSPARENT);
    window.getStylesheets().add("gif.css");
    stage.setScene(window);
    stage.setOpacity(0.5);
    // Get GraphicsContex and set Focus
    gc = canvas.getGraphicsContext2D( );
    gc.setFontSmoothingType(FontSmoothingType.GRAY);
    canvas.addEventFilter(MouseEvent.ANY, (e) -> canvas.requestFocus());

The line window.setFill(Color.TRANSPARENT); and stage.setOpacity(0.5); tell JVM that the Scene is 100% transparent, but the Stage should be semi-transparent" (0.5). The effect is to cover the computer screen with a “haze” so you can continue to see what’s going on underneath. The following codes show you how to focus on a specific area on the computer screen:

  1. Focusing: Draw a red rectangle. After the first mouse click for the starting coordinates (x0/y0), a red rectangle is continuously drawn with the moving mouse pointer.
    canvas.setOnMouseMoved(e -> {
      if (!move || !flag) return;
      int xx = (int)e.getX();
      int yy = (int)e.getY();
      //clear the canvas....
      gc.clearRect(0, 0, wMx, hMx);
      // draw a red rectangle
      gc.setLineWidth(2);
      gc.setStroke(Color.RED);
      gc.strokeLine(x0, y0, xx, y0);
      gc.strokeLine(x0, yy, xx, yy);
      gc.strokeLine(x0, y0, x0, yy);
      gc.strokeLine(xx, y0, xx, yy);
    });
  1. Focus area: With the second mouse click you set the 2nd coordinate of the focus area. The red focus frame (rectangle) disappears (stage.hide() -see Picture in Session 2, FOCUS button) and you can now start and stop taking the screenshot using the START/STOP button.
    canvas.setOnMouseReleased(e -> {
      if (!flag) return;
      int xx = (int)e.getX();
      int yy = (int)e.getY();
      // Starting coordinate?
      if (x0 < 0) {
        x0 = xx;
        y0 = yy;
        move = true;
        return;
      }
      // second coordinate
      move = false;
      flag = false;
      // setting focus area:
      // x0, y0, width and height
      if (x0 > xx) {
        int x = x0;
        x0 = xx;
        xx = x;
      }
      if (y0 > yy) {
        int y = y0;
        y0 = yy;
        yy = y;
      }
      width = xx-x0;
      height = yy-y0;
      // delay 200 ms then hide the canvas stage
      // set focused for taking screenshot
      waiting(200);
      stage.hide();
      disable(false);
      focused = true;
    });
  1. Recording: When the START button is clicked, the virtual camera begins taking snapshots with a 20 ms timeframe from the computer screen (idle = false) and then “extracts” the focus area as a focused image. The button changes its text to STOP. When STOP is clicked, the virtual camera recording ends (done = false).
    ForkJoinPool.commonPool().execute(() -> {
      BufferedImage bImg, image;
      while (running) {
        while (idle)
          waiting(100);
        while (!done) {
          try { // taking snapshots
            bImg = (new java.awt.Robot()).
                   createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()));
            image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
            image.setRGB(0, 0, width, height,
                         bImg.getRGB(x0, y0, width, height, null, 0, width),
                         0, width);
            bImgList.add(image);
            TimeUnit.MILLISECONDS.sleep(20);
          } catch (Exception ex) { }
        }
      }
    });

Note: There’re two ways to copy pixels from one image to another image:

  • the traditional way: slow, but easy to read
            for (int y = y0, b = 0; y < hMx && b < height; ++y, ++b)
              for (int x = x0, a = 0; x < wMx && a < width; ++x, ++a)
                image.setRGB(a, b, bImg.getRGB(x,y));
  • the most effective way: read an array of Source (here: bImg) and add it to the new image (here: image)
            image.setRGB(0, 0, width, height,
                         bImg.getRGB(x0, y0, width, height, null, 0, width), // Source RGB array
                         0, width);

It depends on your coding purpose. The 2nd way is not only effective, but also very fast. For a time frame of this virtual camera, it is crucial to copy the pixels as quickly as possible.

The captured images are stored in an ArrayList. This list can be edited, showed, or saved as a new GIF or GZIP file (suffix .gif or .gzip). Click HERE to see clip giving you an idea of how the virtual camera process works (focusing, start/stop, show).

Before I go into more detail about the two APIs of Camcorder package: GifIO.java and Display.java, let’s take a look at Tools.java. Tools.java provides some convenient static methods that are frequently invoked by CamCorder.java. These static methods are:

  1. getButton: Create JFX button with/without icon and with functional hint
  2. getSlider: Create a JFX slider with a scale between 0.1 and 2.0
  3. getTextField: Create JFX TextField with numeric check if input must be a number.
  4. Dialog: Create JFX dialog frame
  5. setPosition: positioning of the stage
  6. getFile: get a file (with FileChooser)
  7. toFiles: Creates n PNG image files from captured/loaded images
  8. iClone: Clones an image

Note: The image here is BufferedImage (do not confuse with JFX Image).

(to be continued)

(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

1 Like

How to create a Camcoder JAR file?
As I said at the beginning of this tutorial, I will show you the basic work within basic IT software development. Requirement: JDK of a version smaller than JDK11 (better JDK 8 or JDK 9). If you have a higher version JDK, you need to download its corresponding Open JavaFX (click HERE). First, download the gif_sources.zip file which contains 4 Java sources and 1 subdirectory classes:

  • CamCorder.java: the main app
  • Tools.java: the diverse tools
  • Display.java: the animation API
  • GifIO: the GIF-IO stream
  • Subdirectory classes contains 5 files: gif.css (CSS file), manifest.mf (for jar), 3 icons (camcorder.png, camera.png and hourglass.gif).

On WINDOWS:

  1. Create a batch file, for example: buildjar.bat
REM compilation
javac -g:none -d ./classes *.java
REM switch to directory classes
cd classes
REM creates a JAR file named Camcorder.jar with the output o.txt. Both are in the parent directory
jar -cvfm ../CamCorder.jar manifest.mf *.class *.png gif.css > ../o.txt
REM back to the parent directory
cd ..
REM runs the jar file
java -jar Camcorder.jar
  1. Open a CMD window (press the key with the Windows symbol and type R. An input box will appear. Type cmd and press ENTER.) CMDwindow
  2. on CMD window: Go to the directory where you downloaded gif_sources.zip and run buildjar.bat!


How do I create a desktop icon CamCorder.jar? The easiest way:

  1. Right click on any existing desktop and copy it Desktop_1
  2. Move the mouse cursor anywhere on the desktop and paste the copy with the right mouse button
  3. Left-click Properties on the pasted copy
  4. Change the properties (see image below)

    Here is the result:
    newDesktopIcon

On LINUX (here; Mint):

  1. Create a batch file, for example: buildjar.sh and make sure that this shell script is executable (see Linux image)
  2. Open a terminal by click LM icon and select terminal icon (with $_)
  3. on terminal: Go to the directory where you downloaded gif_sources.zip and run buildjar.sh (see Linux Image)
# compilation
javac -g:none -d ./classes *.java
# switch to directory classes
cd classes
# creates a JAR file named CamCorder.jar with the output o.txt. Both are in the parent directory
jar -cvfm ../Camcorder.jar manifest.mf *.class *.png gif.css > ../o.txt
# back to the parent directory
cd ..
# runs the jar file
java -jar Camcorder.jar


How do I create a desktop icon CamCorder.jar? The 2 easiest ways:

  1. Direct on Desktop (make sure that directory Desktop exists in your $HOME directory): run sudo nano Desktop/CamCorder.desktop
  2. via Menu-Application: run sudo nano /usr/share/applications/CamCorder.desktop and type or copy/paste the following:
    [Desktop Entry]
    Name=CamCorder
    GenericName=Virtual Camera
    Exec=/home/erika/java/jdk-9.0.4/bin/java -Xms1024m -jar /home/erika/JoeApp/gif/Camcorder.jar
    Terminal=false
    Icon=/home/erika/JoeApp/gif/classes/Camcorder.png
    Type=Application
    Comment=Virtual Camera
    StartupWMClass=JavaJFX
    Categories=Application;

nano
Use CTRL-O to create and **CTRL-S **to save the desktop file, then CTRL-X to exit. This all happens in real time, so you can immediately launch Camcorder.jar from the Desktop or from Menu->Application->CamCorder
Joe

1 Like
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?