从ServletOutputStream输出GIF

时间:2014-12-31 15:50:55

标签: java image spring-mvc servlets gif

我正在编写一个动态生成GIF文件的端点。我会从头开始。

我有一个名为Function的类,它的工作方式类似于抽象类,我有几个类,在本例中为AddFunction,它们代表了一小部分功能。在这种情况下,AddFunction会将一些数字加在一起。当点击结束点时,AddFunction的ID被传递给它(它可以是任何,在这个例子中它是添加功能)。控制器中的代码如下:

/**
 * 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();
}

首先,Function是由它的ID找到的。我已经检查过,正在找到正确的功能。这段代码需要一些验证(例如检查传入的id是一个有效的数字),但我稍后会这样做。然后我抓取servlet输出流并将其传递给函数的getImage方法。此方法生成描述该功能的GIF。此代码如下所示:

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);
}

正如您所看到的,它会忽略这些值,并且目前正在使用库存数据。它创建了一个值数组。这些值中的每一个都出现在GIF的每个帧中。所以我做的是我采用ServletOutputStream并使用ImageIO.createImageOutputStreamImageOutputStream对象包装它。这是在我自己的writeSequenceToImage类中传递到ImageUtils方法时。最后两个值是写入位置的坐标。在这种情况下,图像的垂直中间位于最左侧。 writeSequenceToImage方法的代码如下:

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));
    }
}

在这段代码中,我使用的是类Test(临时名称),其中包含将数据写入GIF文件的代码。我在这里所做的就是循环并将每个值添加到GIF中的帧。可以找到Test类的代码here。我所做的是构建String,因此在我们的示例中,日志将输出:

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.

这将在GIF的每一帧中显示它构建字符串的外观。现在,我将其写入GIF,并且我希望它直接推送到ServletOutputStream,只有当我尝试使用以下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>

我在Chrome中看到以下数据返回:

Data Response

我在网页上看不到任何图片:

View

我尝试了什么

我试图看到回来的大小。为此,我已将ServletOutputStream替换为ByteArrayOutputStream,以便我可以获得数据的大小。如果我这样做,我的代码看起来像这样:

/**
 * 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();
}

日志输出:

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

所以这告诉我它也不是被写的。所以我改变了我的方法并重构了代码,所以我在我的控制器中保留了对ImageOutputStream的引用。这意味着我可以完全控制对象,所以现在输出日志:

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

这令人鼓舞!对于非常简单的GIF,2KB听起来很合适。但是,当我查看谷歌的回复时,类似的故事:

ios response

虽然这次它有内容长度,但没有可用的预览,图像仍然没有出现。

我想知道这里是否有人解决了类似的问题?我怀疑它与GIF的编码有关,但ImageIO不支持从一个流到另一个流的转换,只支持从一个BufferedImage类型到另一个。因此,我使用ImageIO.read方法将其读入BufferedImage并使用ImageIO.write将其作为gif写入ServletOutputStream。这产生了以下错误:

java.lang.IllegalArgumentException: image == null!

此时,我很难过。我希望一双新鲜的眼睛可以帮助我。有没有人有任何想法?

2 个答案:

答案 0 :(得分:3)

正如评论中已经提到的那样,你的问题有点不合理,但我会试着告诉你它是如何运作的。

首先尝试以下操作:

@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();
}

将一些名为bla.jpg的文件放在根目录中,或将路径更改为某个现有图像文件(也可以是GIF)。确保您至少具有读取权限。

无论是jpg还是gif文件,这都应该适用。如果这不起作用,那么Spring配置可能有问题。你应该排除它。

如果此方法有效,您可以使用方法generateAndWriteToImage()替换ImageIO.read(new File("/bla.jpg"));。你应该完成。

现在,我不知道你的generateAndWriteToImage()方法做了什么,但我假设它创建了一个BufferedImage的实例,将一些文本写入图像并返回它。 像这样:

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;
}

如果使用类型BufferedImage.TYPE_INT_RGB创建图像,则不应导致任何问题。





TL; DR 您已经发现的另一件事是,通过一点点重构,您就可以关闭ImageOutputStream

假设以下方法:

@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();
}

和这个方法:

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);
}

在第二种方法中,您正在使用ImageOutputStream(最后一行)创建ImageIO.createImageOutputStream(out)的本地实例。

我想主要的问题是,你没有关闭这个ImageOutputStream,这可能导致数据没有被写入任何其他OutputStream(因为缓冲)。

要使其有效,您可以将方法重构为:

@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();
}

和此:

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);
}

这同样适用于generateAndWriteToImage()方法。如果它正确返回BufferedImage的实例,这应该有效(使用重构)。

答案 1 :(得分:0)

我没有尝试使用spring,而是尝试使用J2EE。以下方法对我有用!

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();
    }
}