Reputation: 27356
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:
And I am seeing no image on my page:
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:
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
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
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