BCqrstoO BCqrstoO - 3 months ago 42
HTTP Question

SprintBoot returning a PNG from a Controller's RequestMapping

I've been scouring the internet for resources and I feel like I almost have the answer, but I can't quite seem to get a

BufferedImage
to be returned to a browser window.

The project generates a maze which can then create a
BufferedImage
.

Here is the code from my Controller.

@RequestMapping(method = RequestMethod.GET, path = "/image", params = {"rows", "columns"})
public ResponseEntity<byte[]> image(@RequestParam(name = "rows") int rows, @RequestParam(name = "columns") int columns) throws IOException, InterruptedException {
try {
BasicCartesianGrid requestedMaze = new BasicCartesianGrid(rows, columns);
requestedMaze.forEach(CellAlgorithms.BINARY_TREE);
BufferedImage bufferedImage = requestedMaze.toDisplayImage();
{ // Dumping to file for debugging <- this works as expected
File outputFile = new File("save.png");
ImageIO.write(bufferedImage, "png", outputFile);
}
ByteArrayOutputStream pngByteStream = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "png", pngByteStream);
byte[] pngBytes = pngByteStream.toByteArray();
final HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
headers.setContentLength(pngBytes.length);
headers.setCacheControl(CacheControl.noCache().getHeaderValue());

return new ResponseEntity<>(pngBytes, headers, HttpStatus.OK);
} catch (Exception e) {
// This hasn't occurred yet, but is for just in case
Thread.sleep(1000);
System.err.println(e.getLocalizedMessage());

final HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
return new ResponseEntity<>(e.getLocalizedMessage().getBytes("ASCII"), headers, HttpStatus.INTERNAL_SERVER_ERROR);
}
}


I have ascertained that the PNG is being generated correctly, as the file exists and is viewable on my hard-drive. My browser gets a broken image returned back. From my terminal, I can get some more information.

curl "http://localhost:8080/maze/image?rows=10&columns=10"


Dumps out the following (the quotation marks are part of the response, while the data represented by the ellipsis changes from request to request, due to the fact each maze is randomly generated and unique):

"iVBORw0KGgoAAAANSUhEUgAAA......"


I googled this string prefix, and found this page. Which shows that this string should be used as a data-uri, like so:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA…" >


I'm not sure where to go from here. It seems like my image is being generated correctly, but I must be missing a header in my response to tell the browser/spring that these bytes should be interpreted as an image and not as just a string.

UPDATE:
Based on the dialog between myself and
Shawn Clark
from the answer section, here is what I have presently.

@SpringBootApplication
@Log4j
public class SpringMazesApplication {

@Bean
public HttpMessageConverter<BufferedImage> bufferedImageHttpMessageConverter() {
log.debug("Registering BufferedImage converter");
return new BufferedImageHttpMessageConverter();
}

public static void main(String[] args) throws IOException {
SpringApplication.run(SpringMazesApplication.class, args);
}
}


And the actual controller:

@Controller
@RequestMapping(path = "/maze/basic", method = RequestMethod.GET)
@Log4j
public class BasicMazeController {

@RequestMapping(params = {"format", "format=text"}, produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
public String simpleMazeText(@RequestParam(name = "rows", defaultValue = "10", required = false) int rows,
@RequestParam(name = "columns", defaultValue = "10", required = false) int columns) throws IOException {
BasicCartesianGrid requestedMaze = new BasicCartesianGrid(rows, columns);
requestedMaze.forEach(CellAlgorithms.BINARY_TREE);
return requestedMaze.toDisplayString();
}

@RequestMapping(params = {"format=image"}, produces = MediaType.IMAGE_PNG_VALUE)
@ResponseBody
public BufferedImage simpleMazeImage(@RequestParam(name = "rows", defaultValue = "10", required = false) int rows,
@RequestParam(name = "columns", defaultValue = "10", required = false) int columns) throws IOException {
log.debug("Starting image generation");
BasicCartesianGrid requestedMaze = new BasicCartesianGrid(rows, columns);
requestedMaze.forEach(CellAlgorithms.BINARY_TREE);
BufferedImage bufferedImage = requestedMaze.toDisplayImage();
{ // Dumping to file for debugging <- this works as expected
log.debug("Dumping image to hd");
File outputFile = new File("save.png");
ImageIO.write(bufferedImage, "png", outputFile);
}
log.debug("Returning from image generation");
return bufferedImage;
}

@RequestMapping
@ResponseBody
public ResponseEntity<String> simpleMazeInvalid(@RequestParam(name = "rows", defaultValue = "10", required = false) int rows,
@RequestParam(name = "columns", defaultValue = "10", required = false) int columns,
@RequestParam(name = "format") String format) throws IOException {
final HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
return new ResponseEntity<>("Invalid format: " + format, headers, HttpStatus.BAD_REQUEST);
}
}


From my terminal I can
curl -D - "url"
and I can see with both logging/debugging and the output from my terminal, that the converter is properly registered at the begging of the application and that I'm getting responses as you would expect from all but the actual image uri which returns a
406 Not Acceptable
. If I remove the
@ResponseBody
from the image method, it just returns a
500
. I can verify that the image is properly generated as it is being written to disk as I expect it should.

Terminal output from curling my restpoints

Answer

Check out the produces attribute on the @RequestMapping. You would want to set it to image/png.

Here is a complete example:

@RestController
public class ProduceImage {
    @GetMapping(path = "/image", produces = "image/png")
    public BufferedImage image() throws Exception {
        BufferedImage bufferedImage = ImageIO.read(new File("E:\\Downloads\\skin_201305121633211421.png"));
        return bufferedImage;
    }
}

My BufferedImage is something from my computer but it can be just as easily the BufferedImage that you have from the requestedMaze.toDisplayImage() without having to do all that other work. To make this work you want to include the BufferedImageHttpMessageConverter in your context.

@Bean
public HttpMessageConverter<BufferedImage> bufferedImageHttpMessageConverter() {
    return new BufferedImageHttpMessageConverter();
}