【问题标题】:Output GIF from ServletOutputStream从 ServletOutputStream 输出 GIF
【发布时间】:2015-02-27 16:08:34
【问题描述】:

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

我有一个名为Function 的类,它像一个抽象类一样工作,我有几个类,在这个例子中是AddFunction,它们代表一小块功能。在这种情况下,AddFunction 将一些数字相加。当到达终点时,AddFunction 的 ID 被传递给它(它可以是任何,在这个例子中它是 add 函数)。控制器中的代码如下:

/**
 * 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 对象包装它。这是在我自己的ImageUtils 类中传递给writeSequenceToImage 方法时。最后两个值是写入位置的坐标。在这种情况下,图像的垂直中间,在最左边。 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 找到。我所做的是构建字符串,因此在我们的示例中,日志将输出:

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 中看到以下数据返回:

我在我的页面上看不到任何图像:

我的尝试

我试图查看返回的内容的大小。为此,我将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 听起来很合适。然而,当我查看谷歌的回复时,类似的故事:

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

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

java.lang.IllegalArgumentException: image == null!

在这一点上,我很难过。我希望有一双新的眼睛可以帮助我。有人有什么想法吗?

【问题讨论】:

  • 如果你把字节数组写成gif文件,你能打开它并正确渲染吗?另外,为什么内容类型是 image/webp?不应该是在Controller方法中注解的image/gif?
  • 应该是。如果没有返回数据,它会以text/plain 出现,这更奇怪!我现在做一个测试,然后为你找出答案。
  • 你一次尝试的太多了。这使得解决问题变得困难。你可能有几个。创建一个将 GIF 保存在文件中的测试(与 Spring MVC 框架分开)并验证它是否有效。创建一个更简单的控制器方法,从磁盘读取工作中的 GIF 并将其作为响应发送。
  • 我可以发现至少两个问题:(1)您没有关闭 ImageOutputStream 实例。因此,部分图像可能会丢失。 (2) 在getImage 方法的第一个版本中,您同时使用HttpServletResponseproduces。如果我没记错的话,它们不会混合。如果使用HttpServletResponse,则必须在响应中设置内容类型和内容长度。
  • 如果你们能为 A)生成 GIF 和 B)在 HTTP 响应中编写它提供一套清晰的步骤,那么您将获得赏金。我在 Stack Overflow 上的任何地方都没有找到这个,所以我认为我们把它记下来很重要。

标签: java image spring-mvc servlets gif


【解决方案1】:

正如 cmets 中已经指出的那样,您的问题有点不简洁,但我会尝试向您展示它是如何工作的。

首先尝试以下方法:

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

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

我想主要的问题是,你没有关闭这个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 的实例,这应该可以工作(通过重构)。

【讨论】:

  • 您的提示引导我找到正确的答案,我觉得这对于任何返回此问题的人来说都是一个非常好的指南。好赚!
  • 很高兴能帮上忙 :)
【解决方案2】:

我没有尝试使用 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();
    }
}

【讨论】:

  • 好的,这种方法的问题是您从文件中获取 GIF 并在响应中输出它。我正在寻找如何使用代码生成它。
  • 我只是想帮助解决损坏的图像部分。就这样。应该有一些教程和示例供您学习如何在 java 中创建图像。喜欢here
  • 不,我同意,您的意见很好。但我正在寻找的是对问题的完整、独立、逐步的解释。上面提供了一个很好的“故障排除”格式。都一样,+1 表示有效信息。编辑:顺便说一句,我没有投反对票。
猜你喜欢
  • 1970-01-01
  • 2023-03-15
  • 2015-04-23
  • 2012-06-17
  • 2014-09-14
  • 1970-01-01
  • 1970-01-01
  • 2016-01-06
  • 2021-08-28
相关资源
最近更新 更多