/****************************************************************************************/ /** J.Anders * Germany * TU-Chemnitz-Zwickau */ /****************************************************************************************/ /** Fakultaet fuer Informatik * Professur "Rechnernetze und */ /** * verteilte Systeme" */ /****************************************************************************************/ /** Telefon: +49 0371 531 1360 * Fax: +49 371 531 1628 */ /****************************************************************************************/ /* */ /* This file contains the whole source code for a FLI/FLC file player in JAVA. */ /* Parameters: */ /* FILENAME: URL of the FLI/FLC file (relative or absolut) */ /* DELAY (optional): delay (in ms) between 2 frames */ /* during animation (default: 200) */ /* */ /* The FLI/FLC player works in 3 steps: */ /* 1. Loading and parsing of the FLI/FLC file; */ /* 2. Reconstruction of the single frames; */ /* 3. Playing of the animation in a thread; */ /* */ /****************************************************************************************/ /*+++++++++++++++++++++++++++ library modules ++++++++++++++++++++++++++++++++++++++++++*/ import java.io.*; import java.net.*; import java.applet.Applet; import java.awt.*; import java.awt.image.IndexColorModel; import java.awt.image.ColorModel; import java.awt.image.ImageProducer; import java.awt.image.ImageConsumer; /*++++++++++++++++++++++++++ auxiliary classes containing data ++++++++++++++++++++++++++*/ class Err { // contains a possible error message; public static String Msg = null; } class Pixel_List { // to build a list of frame data public Pixel_List next = null; // (one element per frame) public byte color_values[]; public IndexColorModel curr_Model; // the current color model Pixel_List (Pixel_List predecessor, IndexColorModel m, int width, int height) { color_values = new byte[width * height]; if (m != null) { // new color model ? curr_Model = m; // adopt } if (predecessor != null) { // If a predecessor frame is available its data are copied. That's because in general the // FLI/FLC file contains a sequence of differences in respect of the predecessor frame. System.arraycopy(predecessor.color_values, 0, color_values, 0, color_values.length); // If no new color model is specified the color model of the predecessor frame is used: if (m == null) curr_Model = predecessor.curr_Model; predecessor.next = this; } } } /*++++++++++++++++++++++++++++++++++++++ classes +++++++++++++++++++++++++++++++++++++++*/ /* The class "Flic_Player" implements the applet. It produces an object "scan" of the */ /* class "Scanner" and passes the FLC/FLI data to it. After that the variables: */ /* scan.frame_counter */ /* scan.width */ /* scan.height */ /* are set according to the FLI/FLI file data and the variable "scan.anchor" points to */ /* a list of frame data. */ public class Flic_Player extends Applet implements Runnable { private int curr_nr = 0; // current frame (during animation) private Scanner scan; // THE scanner private Thread AnimatorThread; // the animation runs as thread boolean painted = false; // "true", if the frame "curr_nr" is painted private int delay = 200; // delay between 2 frames (in ms) private Image Images[]; // the sequence of images private boolean io = true; // "false" in case of errors public void init () { // overwrites the method "init()" of Applet String filename = null; // THE Name of the FLI/FLC file String s_delay = null; // delay (as string) URL source_url; // THE source (as URL) Pixel_List pl; // auxiliary variable to trace the list of images DataInputStream in_stream = null; // THE input stream try { // obtain parameters filename = getParameter("FILENAME"); s_delay = getParameter("DELAY"); if (s_delay != null) { try { delay = Integer.parseInt(s_delay); } catch (NumberFormatException e) { Err.Msg = s_delay + " is not a number"; io = false; } } } catch (NullPointerException e) { Err.Msg = "no parameter \"FILENAME\""; io = false; } if (filename == null) { Err.Msg = "no parameter \"FILENAME\""; io = false; } if (io) { try { // construct URL and open the input stream: source_url = new URL(getCodeBase(), filename); in_stream = new DataInputStream(source_url.openStream()); } catch (MalformedURLException e) { Err.Msg = "MalformedURLException"; io = false; } catch (IOException e) { Err.Msg = "Cannot open: " + getCodeBase() + "/" + filename; io = false; } } if (io) { scan = new Scanner(in_stream); // produce the scanner and pass io = scan.scan_flic(); // the input stream to it if (io) { resize(scan.width, scan.height); // but now possible because of Images = new Image[scan.frame_counter]; // "frame_counter" pl = scan.anchor; // trace the list for (int i = 0; i < scan.frame_counter && io; i++) { if (pl == null) { // bad "frame_counter" ? Err.Msg = "error in tracing the list"; io = false; } /*+++ produce one image per frame +++*/ Images[i] = createImage( new myProducer(pl.color_values, scan.width, scan.height, pl.curr_Model)); pl = pl.next; // continue } } } } /*+++ The animation must be performed as thread because the +++*/ /*+++ web browser expects that the "start()" method returns +++*/ /*+++ after finite time. An infinite animation loop would +++*/ /*+++ violate against this principle. +++*/ public void run() { long starttime = System.currentTimeMillis(); // notice the start time while (Thread.currentThread() == AnimatorThread) { // Is it me ? repaint(); // sets the flag for "update()" --> "paint()" painted = false; try { // "sleep()" throws the "InterruptedException" starttime += delay; // compute the destination time // if destination time isn't reached --> sleep Thread.sleep(Math.max(0,starttime - System.currentTimeMillis())); } catch (InterruptedException e) { // must be; otherwise it leads to an error break; // because "sleep()" throws the "InterruptedException" } /* "curr_nr" is only incremented if the Web browser */ /* had the opportunity to paint the current image. */ /* ("repaint()" sets only a flag: "Please repaint */ /* occasionally!") */ if (painted) curr_nr++; } } /*+++ It is guaranteed that "start()" is called after "init()". +++*/ /*+++ That means the image sequence list (anchor) already exists. +++*/ /*+++ The "start()" method leaves behind only a runnable thread +++*/ /*+++ and returns. +++*/ public void start() { // overwrites the "start()" method of the Applet if (!io) return; if (AnimatorThread == null) { // make sure there is no animation AnimatorThread = new Thread(this); // produce the thread AnimatorThread.start(); // pass the thread to the scheduler } } /*+++ The "update()" method is called at the first opportunity +++*/ /*+++ after "repaint()" has set the "Please repaint!" - Flag. +++*/ public void update(Graphics g) { // overwrites the "update()" - method of Applet // Put frame (curr_nr % scan.frame_counter) to foreground g.drawImage(Images[curr_nr % scan.frame_counter], 0, 0, this); painted = true; // "is painted" --> see "run()" } // "stop()" overwrites the "stop()" method of the Applet public void stop() { // is called if the page is unloaded AnimatorThread = null; // IMPORTANT: otherwise the animation continues in background !!! } /*+++ "paint()" is actually called after "update()" has cleared the +++*/ /*+++ screen. Because "update()" is overwritten "paint()" is only called +++*/ /*+++ at the beginning and in case of exposure events. At this +++*/ /*+++ opportunity a possible error message is displayed. +++*/ public void paint(Graphics g) { // overwrites the "paint()" method of the Applet if (Err.Msg != null) { g.setFont(new Font(g.getFont().getName(), Font.BOLD, 10)); g.drawString(Err.Msg, 10, 100); return; } g.drawImage(Images[curr_nr % scan.frame_counter], 0, 0, this); } } /*+++ The class "Scanner" provides the methods suitable to parse the FLI/FLC +++*/ /*+++ coded data. As a result of calling "scan_flic" the following (public) +++*/ /*+++ variables are set accordingly to the values in FLI/FLC file: +++*/ /*+++ +++*/ /*+++ frame_counter number of frames +++*/ /*+++ width width of one frame +++*/ /*+++ height height of one frame +++*/ /*+++ +++*/ /*+++ Furthermore the variable "anchor" points to the beginning of a list of +++*/ /*+++ image data. Each image is stored as a sequence of pixel indexes in +++*/ /*+++ left-right/top-down manner. The indexes refer to an index color model +++*/ /*+++ which is stored in this list, too. +++*/ class Scanner { /*+++ FLI/FLC constants: +++*/ final private int FLI_MAGIC = 0xAF11; final private int HEADER_LENGTH = 26 + 102; final private int FRAME_HEADER_LENGTH = 16; final private short M_FLI = (short) 0xAF11; final private short M_FLC = (short) 0xAF12; final private short FLI_COLOR_256 = (short) 4; final private short FLI_COLOR = (short) 11; final private short FLI_LC = (short) 12; final private short FLI_WORD_LC = (short) 7; final private short FLI_BLACK = (short) 13; final private short FLI_BRUN = (short) 15; final private short FLI_COPY = (short) 16; final private short FRAME_ID = (short) 0xf1fa; /*+++ public variables (will be set reasonably): +++*/ public Pixel_List anchor = null; public int width, height, frame_counter = 0; /*+++ private variables: +++*/ private boolean io = true; // "false" if syntax error private byte buffer[]; // portion of source data private DataInputStream In_str; // THE input stream private Pixel_List last = null; // end of the list of image data private IndexColorModel myModel; // "null" if no new color model specified private byte r[] = new byte[256]; // sequence of red values in index color model private byte g[] = new byte[256]; // sequence of green values in index color model private byte b[] = new byte[256]; // sequence of blue values in index color model Scanner(DataInputStream f_In_str) { In_str = f_In_str; // notice the input stream } /* The method "scan_fli()" reads the content of the FLI/FLC-header. */ /* It sets the variables "width" and "height". */ /* The return value is true if no parsing error occurs. */ public boolean scan_flic() { int length; // length of file short magic, size, speed; // read the whole header (fix length) to the"buffer": buffer = new byte[HEADER_LENGTH]; try { ReadContent(In_str, buffer, HEADER_LENGTH); } catch (IOException e) { Err.Msg = "read error 1"; return false; } // extract content: length = to_int(buffer, 4, 0); magic = (short) to_int(buffer, 2, 4); size = (short) to_int(buffer, 2, 6); width = (short) to_int(buffer, 2, 8); height = (short) to_int(buffer, 2, 10); speed = (short) to_int(buffer, 2, 16); switch (magic) { // OK ? case M_FLI: break; case M_FLC: break; default: Err.Msg = "Number " + magic + "unknown"; return false; } length -= HEADER_LENGTH; // remaining length // The frame count isn't specified in the header. That's why // the end of file must be computed by means of file length. // Such the frame count is computed implicitely. while (length > 0 && io) { length -= scan_frame(); // The "frames_counter" cannot be incremented here // because a frame not necessarily contains a an image // but only a color model. } frame_counter--; // erase the loop frame return io; } /* The method "scan_frame()" reads data of one frame. A frame */ /* contains a number of chunks. Such a chunk contains either */ /* pixel data (FLI_BRUN), pixel difference data (FLI_LC in FLI or */ /* FLI_WORD_LC in FLC) or a color map (FLI_COLOR in FLI or */ /* FLI_COLOR_256 in FLC). FLI_COPY, FLI_BLACK and FLI_PREVIEW data */ /* are ignored and I hope they will never occur. */ /* The return value is the amount of data in bytes. The method */ /* changes possibly the value of "io". */ private int scan_frame() { int frame_size; short fh_id, chunks; // read the whole frame header (fix length) to the"buffer": buffer = new byte[FRAME_HEADER_LENGTH]; try { ReadContent(In_str, buffer, FRAME_HEADER_LENGTH); } catch (IOException e) { Err.Msg = "read error 2"; io = false; return 0; } // extract data: frame_size = to_int(buffer, 4, 0); fh_id = (short) to_int(buffer, 2, 4); // tag chunks = (short) to_int(buffer, 2, 6); // count of chunks // First the color model is set to "null". If an FLI_COLOR(_256) // appears "myModel" is changed accordingly. myModel = null; if (fh_id != FRAME_ID) { // check ! Err.Msg = "FLI/FLC synchronization error"; io = false; return 0; } for (int ch = 0; ch < chunks; ch++) { // read the chunk data scan_chunk(); } return frame_size; } /* The method "scan_chunk()" reads one chunk. Depending on the chunk */ /* type further methods are called. */ private void scan_chunk() { int ch_size; short ch_type; // read the chunk header (fix length) to the "buffer": buffer = new byte[6]; try { ReadContent(In_str, buffer, 6); } catch (IOException e) { Err.Msg = "read error 3"; io = false; return; } // extract data: ch_size = to_int(buffer, 4, 0); ch_type = (short) to_int(buffer, 2, 4); // read the remaining data to the "buffer": buffer = new byte[ch_size - 6]; try { ReadContent(In_str, buffer, ch_size - 6); } catch (IOException e) { Err.Msg = "read error 4"; io = false; return; } switch (ch_type) { // dectect chunk type case FLI_COLOR: scan_FLI_COLOR(2);break; case FLI_COLOR_256: scan_FLI_COLOR(0);break; case FLI_LC: scan_FLI_LC(); break; case FLI_WORD_LC: scan_FLI_WORD_LC(); break; case FLI_BRUN: scan_FLI_BRUN(); break; case FLI_BLACK:break; case FLI_COPY: break; default: Err.Msg = "chunk type " + ch_type + " unknown"; io = false; return; } } /* The method "scan_FLI_BRUN()" reads FLI_BRUN data. These data des- */ /* cribe an image pixel by pixel. The data are possibly subdivided into */ /* packets. In the case of a sequence of equal pixel values only the */ /* pixels value and the length of the sequence is given. */ /* It is assumed that the FLI_BRUN data are given first. Otherwise the */ /* player will fail. */ private void scan_FLI_BRUN() { int idx = 0; // offset in data buffer int ppx; // offset in frame int pkts; // count of packets in a line int xchpx; // current pixel value int pixel; // pixel counter int line; // current line byte f; // to remember a pixel value short line_count = (short) height; if (frame_counter > 0) { // see above ! Err.Msg = "FLI_BRUN although frame_counter > 0"; io = false; return; } if (myModel == null) { // no color model ??? Err.Msg = "No color model for the first frame ?"; io = false; return; } // produce the first element in the list of frames: last = anchor = new Pixel_List(null, myModel, width, height); frame_counter++; idx = 0; for (line = 0; line < line_count; line++) { pkts = (0xff & buffer[idx++]); ppx = line * width; for (int pkt_nr = 0; pkt_nr < pkts; pkt_nr++) { xchpx = (int) buffer[idx++]; if (xchpx >= 0) { // set the following byte "xchpx" times f = (byte) (0xff & buffer[idx++]); for (int k = 0; k < xchpx; k++) { anchor.color_values[ppx++] = f; } } else { // the following "xchpx" bytes are pixel values xchpx = -xchpx; for (pixel = 0; pixel < xchpx; pixel++) { anchor.color_values[ppx++ ] = (byte)(0xff & buffer[idx++]); } } } } } /* The method "scan_FLI_LC()" recognizes FLI_LC data which occur in FLI */ /* files. They describe the differences in respect of the predecessor */ /* frame. After a possible skip of (unchanged) data at the beginning */ /* follow the data of "line_count" lines. The data of every line are */ /* possibly subdivided into packets with (possibly) some unchanged */ /* pixels between them. */ /* It is assumed that FLI_LC data never occur first. Otherwise the */ /* player will fail. */ private void scan_FLI_LC() { int idx = 0; // offset in buffer int ppx; // offset in frame int pkts; // count of data packets in a line int xchpx; // current value int pixel; // pixel counter int line; // current line short line_count; // count of lines to change (after a possbile skipping) byte f; // ro remember a pixel value idx = 0; if (frame_counter == 0) { // see above ! Err.Msg = "FLI_LC although frame_counter == 0"; io = false; return; } // produce the next element in the list of frames: // (The constructor copies the data of the predecessor // frame "last") last.next = new Pixel_List(last, myModel, width, height); frame_counter++; last = last.next; // update line = to_int(buffer, 2, idx); idx += 2; line_count = (short) to_int(buffer, 2, idx); idx += 2; for (; line < line_count; line++) { pkts = (0xff & buffer[idx++]); ppx = line * width; for (int pkt_nr = 0; pkt_nr < pkts; pkt_nr++) { ppx += (0xff & buffer[idx++]); // skip pixels xchpx = (int) buffer[idx++]; if (xchpx < 0) { // set the following value "xchpx" times xchpx = -xchpx; f = (byte) (0xff & buffer[idx++]); for (int k = 0; k < xchpx; k++) { last.color_values[ppx++] = f; } } else { // the following "xchpx" bytes are pixel values for (pixel = 0; pixel < xchpx; pixel++) { last.color_values[ppx++ ] = (byte)(0xff & buffer[idx++]); } } } } } /* The method "scan_FLI_WORD_LC()" recognizes FLI_WORD_LC data which */ /* occur in FLC files. These are pixel difference values in respect of */ /* the predecessor frame. The pixel values of "line_count" consecutive */ /* lines are specified followed by a (possible) skip of (unchanged) */ /* lines. The pixel values are given in double bytes. In the case of an */ /* odd number of pixels per line the remaining pixel is given in a */ /* special manner. */ /* It is assumed that FLI_WORD_LC data never occur first. Otherwise the */ /* player will fail. */ private void scan_FLI_WORD_LC() { int idx = 0; // offset in buffer int ppx; // offset in frame int xchpx; // current value int pixel; // pixel counter int line; // line counter int yoff; // the real current line short line_count; // count of lines to change short pkts; // count of packets per line byte f1, f2; // ro remember 2 pixel values boolean last_pix_flag = false; // is a last single pixel stored ? byte last_pixel = 0; // value of a last single pixel idx = 0; if (frame_counter == 0) { // see above! Err.Msg = "FLI_LC_WORD although frame_counter == 0"; io = false; return; } // produce the next element in the list of frames: // (The constructor copies the data of the predecessor // frame "last") last.next = new Pixel_List(last, myModel, width, height); frame_counter++; last = last.next; // update line_count = (short) to_int(buffer, 2, idx); idx += 2; yoff = 0; for (line = 0; line < line_count; line++) { pkts = (short) to_int(buffer, 2, idx); idx += 2; while ((pkts & 0x8000) != 0) { if ((pkts & 0x4000) != 0) { // no packet counter but ... yoff -=(int) pkts; // ...count of lines to skip } else { // ...value of a last single pixel last_pix_flag = true; // notice! last_pixel = (byte) (pkts & 0xff); } // the packet count follows pkts = (short) to_int(buffer, 2, idx); idx += 2; } ppx = yoff * width; for (int pkt_nr = 0; pkt_nr < pkts; pkt_nr++) { ppx += (0xff & buffer[idx++]); // skip pixels xchpx = (int) buffer[idx++]; if (xchpx < 0) { // set the following 2 bytes "xchpx" times xchpx = -xchpx; f1 = (byte) (0xff & buffer[idx++]); f2 = (byte) (0xff & buffer[idx++]); for (int k = 0; k < xchpx; k++) { last.color_values[ppx++] = f1; last.color_values[ppx++] = f2; } } else { // "xchpx" double bytes follow for (pixel = 0; pixel < xchpx; pixel++) { last.color_values[ppx++ ] = (byte)(0xff & buffer[idx++]); last.color_values[ppx++ ] = (byte)(0xff & buffer[idx++]); } } } if (last_pix_flag) { // is there a value of a last single pixel ? last.color_values[(yoff + 1) * width - 1] = last_pixel; last_pix_flag = false; } yoff++; // count the lines } } /* The method "scan_FLI_COLOR(int shift)" recognizes FLI_COLOR data as */ /* well as FLI_COLOR_256 data. The latter occur in FLC files, the other */ /* in FLI files. The data describe a color map with 256 color values */ /* maximal. The difference is: The RGB values in FLI_COLOR_256 are 8 */ /* bit values; the RGB values in FLI_COLOR are 6 bit values. That means */ /* the FLI_COLOR data must be "shift"-ed 2 bytes to the left. The */ /* FLI_COLOR(_256) data are (eventually) subdivided into packets and are*/ /* (possibly) differences in respect of the predecessor color map. */ /* Therefore a skipping is possible. Since the arrays "r", "g" and "b" */ /* are private global variables it is guaranteed that the new values */ /* are always built in recpect to the existing one. */ /* The method produces a new color model "myModel". */ private void scan_FLI_COLOR(int shift) { short pkts; // count of packets int skip; // count of values to skip int count; // count of values in a packet int idx = 0; // offset in buffer int table_idx = 0; // index in color map pkts = (short) to_int(buffer, 2, 0); idx += 2; for (int pkt_nr = 0; pkt_nr < pkts; pkt_nr++) { skip = 0xff & (buffer[idx++]); count= 0xff & (buffer[idx++]); table_idx += skip; if (count == 0) count = 256; // zero means 256 !!! for (int j = 0; j < count; j++) { // read RGB values r[table_idx ] = (byte) ((0xff & buffer[idx++]) << shift); g[table_idx ] = (byte) ((0xff & buffer[idx++]) << shift); b[table_idx++] = (byte) ((0xff & buffer[idx++]) << shift); } } // After reading the RGB values a new index color model is produced: myModel = new IndexColorModel(8, table_idx, r, g, b); } /* The method "ReadContent (DataInputStream Stream, byte field[], int size)" */ /* reads from "Stream" into the "field" until "size" bytes are read or a read */ /* error occurs. */ private void ReadContent (DataInputStream Stream, byte field[], int size) throws IOException { int bytes_read = 0; while (bytes_read < size) { bytes_read += Stream.read(field, bytes_read, size - bytes_read); } } /* Because JAVA has no pointers and no unsigned types a special method */ /* "int to_int(byte b[], int length, int off)" */ /* is necessary which reads "length" Bytes at position "off" from "b" and */ /* translates it to a "length" byte integer value. */ private int to_int(byte b[], int length, int off) { int r_val = 0; for (int i = 0; i < length; i++) { r_val <<= 8; r_val |= b[length - 1 - i + off] & 0xff; } return r_val; } } /*+++ JAVA offers 2 possibilities to create an image: +++*/ /*+++ +++*/ /*+++ 1. createImage(int width, int height); +++*/ /*+++ 2. createImage(ImageProducer producer); +++*/ /*+++ +++*/ /*+++ The first method isn't suitable here because it requires to draw the +++*/ /*+++ pixel values by means of a sequence of "draw...()" methods into the image.+++*/ /*+++ Since all the pixel values and the index color model are known the images +++*/ /*+++ are produced according to the second method by means of objects of the +++*/ /*+++ class "myProducer" which implements the interface "ImageProducer". +++*/ /*+++ An image producer is an object which supplies the pixel values on demand +++*/ /*+++ of an image consumer. The pixel values are delivered in recpect of a +++*/ /*+++ certain color model which must be known to the image consumer, too. +++*/ /*+++ Therefore the image producer must also supply the color model on demand. +++*/ class myProducer implements ImageProducer { private byte Pix_Data[]; // THE pixel values private int width, height; // THE dimensionens ColorModel Model; // THE color model // The constructor only assures that the image producer // notices the pixel values, the dimension and the // color model. myProducer(byte pixels[], int w, int h, ColorModel cm) { Pix_Data = pixels; // Notice that this is a kind of // "pointer assignement"! Is is // possible because the constructor of // "Pixel_List" copies the pixel values. width = w; height = h; Model = cm; } // Before the first use the image consumer registers itself // to the image producer. // At this opportunity he is informed of the dimension and // the color model. public void addConsumer(ImageConsumer ic) { ic.setDimensions(width, height); ic.setColorModel(Model); } public boolean isConsumer(ImageConsumer ic) { return true; // dummy } public void removeConsumer(ImageConsumer ic) { // dummy } // The following 2 methods are almost identically. // They are called by the image consumer to get the // pixel data: public void requestTopDownLeftRightResend(ImageConsumer ic) { // Tell the image consumer that the pixel data // will be supplied from left to right und from // top to bottom: ic.setHints(ImageConsumer.TOPDOWNLEFTRIGHT); // Send all the pixel data: ic.setPixels(0, 0, width, height, Model, Pix_Data, 0, width); // Tell the image consumer that no further data follow: ic.imageComplete(ImageConsumer.STATICIMAGEDONE); } // The method "startProduction(ImageConsumer ic)" is called by // the image consumer if it produces the image. // For security it is informed of the color model // and the dimensions (I don't know if this is really nenecessary). public void startProduction(ImageConsumer ic) { ic.setDimensions(width, height); ic.setColorModel(Model); requestTopDownLeftRightResend(ic); } }