christopher
christopher

Reputation: 27356

Output GIF from ServletOutputStream

I'm writing an endpoint that dynamically generates a GIF file. I'll go from the ground up.

I have a class named Function that works like an abstract class and I have several classes, in this example AddFunction, that represent small chunks of functionality. In this case, the AddFunction adds some numbers together. When the end point is hit, the ID of the AddFunction is passed to it (it could be any, in this example it's the add function). The code in the controller is as follows:

/**
 * Returns the image for a function
 */
@RequestMapping(value = "/function/{functionId}/image.gif", produces = "image/gif")
public void getImage(@PathVariable(value = "functionId") String functionId, HttpServletResponse response) throws IOException {
    Function function = functionService.getFunction(Integer.valueOf(functionId));

    Logger logger = Logger.getLogger(FunctionController.class);

    ServletOutputStream servOut = response.getOutputStream();

    // Uses default values if you pass in nulls.
    function.getImage(servOut, null, null);

    servOut.flush();
    servOut.close();
}

First, the Function is found by it's ID. I have checked, and the correct function is being found. This code is in need of some validation (for example checking the id passed in is a valid number) but I'll get to that later. I then grab the servlet output stream and pass it to the getImage methods of the function. This method generates the GIF that describes the function. This code looks like this:

public void getImage(OutputStream out, String staticContent, String changedContent) throws IOException {
    String[] data = {"2", "+", "2", "=", "4"};

    Logger logger = Logger.getLogger(AddFunction.class);

    logger.info("Getting the add image.");

    ImageUtils.writeSequenceToImage(ImageIO.createImageOutputStream(out), data, 5, Constants.IMAGE_HEIGHT / 2);
}

As you can see, it ignores the values and it is using stock data at the moment. It creates an array of values. Each of these values which appear in each frame of the GIF. So what I do is I take the ServletOutputStream and I use the ImageIO.createImageOutputStream to wrap that with an ImageOutputStream object. This is when passed into the writeSequenceToImage method in my own ImageUtils class. The last two values are coordinates for where to write from. In this case, the vertical middle of the image, on the far left. The code for the writeSequenceToImage method is as follows:

public static void writeSequenceToImage(ImageOutputStream out, String[] contentList, int x, int y) throws IOException {
    StringBuilder dataBuilder = new StringBuilder();

    Test test = new Test(out, BufferedImage.TYPE_INT_RGB, 500, true);

    Logger logger = Logger.getLogger(ImageUtils.class);

    logger.info("Writing sequence to image.");

    for (String content : contentList) {
        dataBuilder.append(content);

        logger.info("writing " + dataBuilder.toString() + " to the gif.");

        test.writeToSequence(generateAndWriteToImage(dataBuilder.toString(), x, y));
    }
}

In this code, I am using the class Test (temporary name) which contains code that writes data to a GIF file. All I'm doing here is looping through and adding each value to a frame in the GIF. The code for class Test can be found here. What I do is I build up the String, so in our example the logs would output:

2014-12-31 14:37:15 INFO  ImageUtils:48 - Writing sequence to image.
2014-12-31 14:37:15 INFO  ImageUtils:53 - writing 2 to the gif.
2014-12-31 14:37:15 INFO  ImageUtils:53 - writing 2+ to the gif.
2014-12-31 14:37:15 INFO  ImageUtils:53 - writing 2+2 to the gif.
2014-12-31 14:37:15 INFO  ImageUtils:53 - writing 2+2= to the gif.
2014-12-31 14:37:15 INFO  ImageUtils:53 - writing 2+2=4 to the gif.

This will give the appearance in each frame of the GIF of it building up the string. Now, I write this to the GIF and I expect it to be pushed straight into the ServletOutputStream, only when I attempt to reference it with the following HTML:

<div class="panel columns large-12" ng-show="selectedFunction">
    <h2>{{selectedFunction.name}}</h2>

    <p>{{selectedFunction.description}}</p>

    <p>This function expects a {{selectedFunction.expectedParameterType}} as a parameter.</p>

    <p>This function will return a {{selectedFunction.expectedReturnType}}</p>

    <img src="/autoalgorithm/functions/function/{{selectedFunction.id}}/image.gif" alt="{{selectedFunction.name}}"/>
</div>

I am seeing the following data returning in Chrome:

Data Response

And I am seeing no image on my page:

View

What I have tried

I've tried to see the size of what is coming back. To do this, I have replaced the ServletOutputStream with a ByteArrayOutputStream so I can get the size of the data. If I do this, my code looks like this:

/**
 * Returns the image for a function
 */
@RequestMapping(value = "/function/{functionId}/image.gif", produces = "image/gif")
public @ResponseBody byte[] getImage(@PathVariable(value = "functionId") String functionId, HttpServletResponse response) throws IOException {
    Function function = functionService.getFunction(Integer.valueOf(functionId));

    Logger logger = Logger.getLogger(FunctionController.class);

    ByteArrayOutputStream baos = new ByteArrayOutputStream();

    // Uses default values if you pass in nulls.
    function.getImage(baos, null, null);

    logger.info("The number of bytes returned is " + baos.toByteArray().length);

    return baos.toByteArray();
}

And the log outputs:

2014-12-31 15:34:09 INFO  FunctionController:85 - The number of bytes returned is 0

So that is telling me that it isn't being written too. So I changed up my approach and refactored the code so I maintained a reference to the ImageOutputStream in my controller. This meant that I had complete control over the object, so now the log outputted:

2014-12-31 15:39:56 INFO  FunctionController:85 - The number of bytes returned is 2708

Which was encouraging! And 2KB sounds about right for a very very simple GIF. However, when I check the response from Google, similar story:

ios response

Although this time it has a content length, but there is no preview available and the image still is not appearing.

I was wondering if anyone on here had tackled a similar issue? I suspect it is to do with the encoding of the GIF, but ImageIO doesn't support conversion from one stream to another, only from one BufferedImage type to another. So I used the ImageIO.read method to read it into a BufferedImage and used ImageIO.write to write it as a gif onto the ServletOutputStream. This yielded the following error:

java.lang.IllegalArgumentException: image == null!

At this point, I'm stumped. I'm hoping a fresh set of eyes can help me out. Does anyone have any ideas?

Upvotes: 4

Views: 2314

Answers (2)

Ravindra HV
Ravindra HV

Reputation: 2608

I did not try with spring but tried with J2EE instead. Below approach works for me !

private void process(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {

    String baseServletPath = httpServletRequest.getServletContext().getRealPath("/");

    System.out.println("Base Servlet Path :"+baseServletPath);
    String relativeInputFilePath = "images/Tulips.gif";
    String imageFilePath = baseServletPath + relativeInputFilePath;
    File file = new File(imageFilePath);

    BufferedImage bufferedImage = null;
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    try {
        bufferedImage = ImageIO.read(file);
        ImageIO.write(bufferedImage, "GIF", byteArrayOutputStream);
    } catch (IOException e) {
        e.printStackTrace();
    }

    byte[] imageData = byteArrayOutputStream.toByteArray();
    String relativeOutFilePath = "images/TulipsOut.gif";
    String imageOutFilePath = baseServletPath + relativeOutFilePath;
    File fileOut = new File(imageOutFilePath);
    FileOutputStream fileOutputStream=null;
    try {
        fileOutputStream = new FileOutputStream(fileOut);
        fileOutputStream.write(imageData);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    try {
        httpServletResponse.getOutputStream().write( imageData );
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Upvotes: 0

unwichtich
unwichtich

Reputation: 13857

As already noted in the comments your question is a little bit unconcise but I'll try to show you how it can work.

First try the following:

@RequestMapping(value = "/function/{functionId}/image.gif", produces = "image/gif")
public void getImage(@PathVariable(value = "functionId") String functionId, HttpServletResponse response) throws IOException {

    BufferedImage firstImage = ImageIO.read(new File("/bla.jpg"));
    response.setContentType("image/gif"); // this should happen automatically

    ImageIO.write(firstImage, "gif", response.getOutputStream());
    response.getOutputStream().close();
}

Place some file named bla.jpg in your root directory or change the path to some existing image file (can also be a GIF). Make sure you have at least read access rights.

This should work in any case, regardless if jpg or gif file. If this doesn't work there may be something wrong with your Spring configuration. You should rule that out.

If this is working, you can use your method generateAndWriteToImage() to replace ImageIO.read(new File("/bla.jpg"));. And you should be done.

Now, I don't know what your generateAndWriteToImage() method does but I assume that it creates an instance of BufferedImage, writes some text into the image and returns it. Something like this:

public static BufferedImage generateAndWriteToImage(String string, int x, int y) {

    BufferedImage image = new BufferedImage(x,y,BufferedImage.TYPE_INT_RGB);
    Graphics g = image.getGraphics();
    g.setPaintMode();
    g.setFont(g.getFont().deriveFont(30f));
    g.drawString(string, 100, 100);
    g.dispose();
    return image;
}

If you create the image with the type BufferedImage.TYPE_INT_RGB this shouldn't cause any problems.





TL;DR Another thing you already found out yourself is that a little refactoring gives you the ability to close the ImageOutputStream.

Assume the following method:

@RequestMapping(value = "/function/{functionId}/image.gif", produces = "image/gif")
public void getImage(@PathVariable(value = "functionId") String functionId, HttpServletResponse response) throws IOException {
    Function function = functionService.getFunction(Integer.valueOf(functionId));

    ServletOutputStream servOut = response.getOutputStream();

    // Uses default values if you pass in nulls.
    function.getImage(servOut, null, null);

    servOut.flush();
    servOut.close();
}

and this method:

public void getImage(OutputStream out, String staticContent, String changedContent) throws IOException {
    String[] data = {"2", "+", "2", "=", "4"};

    Logger logger = Logger.getLogger(AddFunction.class);

    logger.info("Getting the add image.");

    ImageUtils.writeSequenceToImage(ImageIO.createImageOutputStream(out), data, 5, Constants.IMAGE_HEIGHT / 2);
}

In the second method, you are creating a local instance of ImageOutputStream with ImageIO.createImageOutputStream(out) (last line).

I guess the main problem is, that you aren't closing this ImageOutputStream, this may result in data not beeing written to any other OutputStream (because of buffering).

To make it work you can refactor your methods to this:

@RequestMapping(value = "/function/{functionId}/image.gif", produces = "image/gif")
public void getImage(@PathVariable(value = "functionId") String functionId, HttpServletResponse response) throws IOException {
    Function function = functionService.getFunction(Integer.valueOf(functionId));

    ImageOutputStream servOut = ImageIO.createImageOutputStream(response.getOutputStream());

    // Uses default values if you pass in nulls.
    function.getImage(servOut, null, null);

    servOut.close();
}

and this:

public void getImage(ImageOutputStream out, String staticContent, String changedContent) throws IOException {
    String[] data = {"2", "+", "2", "=", "4"};

    Logger logger = Logger.getLogger(AddFunction.class);

    logger.info("Getting the add image.");

    ImageUtils.writeSequenceToImage(out, data, 5, Constants.IMAGE_HEIGHT / 2);
}

The same thing applies here for the generateAndWriteToImage() method. If it correctly returns an instance of BufferedImage, this should work (with the refactoring).

Upvotes: 3

Related Questions