001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.http.converter; 018 019import java.awt.image.BufferedImage; 020import java.io.File; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.OutputStream; 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.Iterator; 027import java.util.List; 028import javax.imageio.IIOImage; 029import javax.imageio.ImageIO; 030import javax.imageio.ImageReadParam; 031import javax.imageio.ImageReader; 032import javax.imageio.ImageWriteParam; 033import javax.imageio.ImageWriter; 034import javax.imageio.stream.FileCacheImageInputStream; 035import javax.imageio.stream.FileCacheImageOutputStream; 036import javax.imageio.stream.ImageInputStream; 037import javax.imageio.stream.ImageOutputStream; 038import javax.imageio.stream.MemoryCacheImageInputStream; 039import javax.imageio.stream.MemoryCacheImageOutputStream; 040 041import org.springframework.http.HttpInputMessage; 042import org.springframework.http.HttpOutputMessage; 043import org.springframework.http.MediaType; 044import org.springframework.http.StreamingHttpOutputMessage; 045import org.springframework.util.Assert; 046import org.springframework.util.StringUtils; 047 048/** 049 * Implementation of {@link HttpMessageConverter} that can read and write 050 * {@link BufferedImage BufferedImages}. 051 * 052 * <p>By default, this converter can read all media types that are supported 053 * by the {@linkplain ImageIO#getReaderMIMETypes() registered image readers}, 054 * and writes using the media type of the first available 055 * {@linkplain javax.imageio.ImageIO#getWriterMIMETypes() registered image writer}. 056 * The latter can be overridden by setting the 057 * {@link #setDefaultContentType defaultContentType} property. 058 * 059 * <p>If the {@link #setCacheDir cacheDir} property is set, this converter 060 * will cache image data. 061 * 062 * <p>The {@link #process(ImageReadParam)} and {@link #process(ImageWriteParam)} 063 * template methods allow subclasses to override Image I/O parameters. 064 * 065 * @author Arjen Poutsma 066 * @since 3.0 067 */ 068public class BufferedImageHttpMessageConverter implements HttpMessageConverter<BufferedImage> { 069 070 private final List<MediaType> readableMediaTypes = new ArrayList<MediaType>(); 071 072 private MediaType defaultContentType; 073 074 private File cacheDir; 075 076 077 public BufferedImageHttpMessageConverter() { 078 String[] readerMediaTypes = ImageIO.getReaderMIMETypes(); 079 for (String mediaType : readerMediaTypes) { 080 if (StringUtils.hasText(mediaType)) { 081 this.readableMediaTypes.add(MediaType.parseMediaType(mediaType)); 082 } 083 } 084 085 String[] writerMediaTypes = ImageIO.getWriterMIMETypes(); 086 for (String mediaType : writerMediaTypes) { 087 if (StringUtils.hasText(mediaType)) { 088 this.defaultContentType = MediaType.parseMediaType(mediaType); 089 break; 090 } 091 } 092 } 093 094 095 /** 096 * Sets the default {@code Content-Type} to be used for writing. 097 * @throws IllegalArgumentException if the given content type is not supported by the Java Image I/O API 098 */ 099 public void setDefaultContentType(MediaType defaultContentType) { 100 Assert.notNull(defaultContentType, "'contentType' must not be null"); 101 Iterator<ImageWriter> imageWriters = ImageIO.getImageWritersByMIMEType(defaultContentType.toString()); 102 if (!imageWriters.hasNext()) { 103 throw new IllegalArgumentException( 104 "Content-Type [" + defaultContentType + "] is not supported by the Java Image I/O API"); 105 } 106 107 this.defaultContentType = defaultContentType; 108 } 109 110 /** 111 * Returns the default {@code Content-Type} to be used for writing. 112 * Called when {@link #write} is invoked without a specified content type parameter. 113 */ 114 public MediaType getDefaultContentType() { 115 return this.defaultContentType; 116 } 117 118 /** 119 * Sets the cache directory. If this property is set to an existing directory, 120 * this converter will cache image data. 121 */ 122 public void setCacheDir(File cacheDir) { 123 Assert.notNull(cacheDir, "'cacheDir' must not be null"); 124 Assert.isTrue(cacheDir.isDirectory(), "'cacheDir' is not a directory"); 125 this.cacheDir = cacheDir; 126 } 127 128 129 @Override 130 public boolean canRead(Class<?> clazz, MediaType mediaType) { 131 return (BufferedImage.class == clazz && isReadable(mediaType)); 132 } 133 134 private boolean isReadable(MediaType mediaType) { 135 if (mediaType == null) { 136 return true; 137 } 138 Iterator<ImageReader> imageReaders = ImageIO.getImageReadersByMIMEType(mediaType.toString()); 139 return imageReaders.hasNext(); 140 } 141 142 @Override 143 public boolean canWrite(Class<?> clazz, MediaType mediaType) { 144 return (BufferedImage.class == clazz && isWritable(mediaType)); 145 } 146 147 private boolean isWritable(MediaType mediaType) { 148 if (mediaType == null || MediaType.ALL.equals(mediaType)) { 149 return true; 150 } 151 Iterator<ImageWriter> imageWriters = ImageIO.getImageWritersByMIMEType(mediaType.toString()); 152 return imageWriters.hasNext(); 153 } 154 155 @Override 156 public List<MediaType> getSupportedMediaTypes() { 157 return Collections.unmodifiableList(this.readableMediaTypes); 158 } 159 160 @Override 161 public BufferedImage read(Class<? extends BufferedImage> clazz, HttpInputMessage inputMessage) 162 throws IOException, HttpMessageNotReadableException { 163 164 ImageInputStream imageInputStream = null; 165 ImageReader imageReader = null; 166 try { 167 imageInputStream = createImageInputStream(inputMessage.getBody()); 168 MediaType contentType = inputMessage.getHeaders().getContentType(); 169 Iterator<ImageReader> imageReaders = ImageIO.getImageReadersByMIMEType(contentType.toString()); 170 if (imageReaders.hasNext()) { 171 imageReader = imageReaders.next(); 172 ImageReadParam irp = imageReader.getDefaultReadParam(); 173 process(irp); 174 imageReader.setInput(imageInputStream, true); 175 return imageReader.read(0, irp); 176 } 177 else { 178 throw new HttpMessageNotReadableException( 179 "Could not find javax.imageio.ImageReader for Content-Type [" + contentType + "]"); 180 } 181 } 182 finally { 183 if (imageReader != null) { 184 imageReader.dispose(); 185 } 186 if (imageInputStream != null) { 187 try { 188 imageInputStream.close(); 189 } 190 catch (IOException ex) { 191 // ignore 192 } 193 } 194 } 195 } 196 197 private ImageInputStream createImageInputStream(InputStream is) throws IOException { 198 if (this.cacheDir != null) { 199 return new FileCacheImageInputStream(is, cacheDir); 200 } 201 else { 202 return new MemoryCacheImageInputStream(is); 203 } 204 } 205 206 @Override 207 public void write(final BufferedImage image, final MediaType contentType, 208 final HttpOutputMessage outputMessage) 209 throws IOException, HttpMessageNotWritableException { 210 211 final MediaType selectedContentType = getContentType(contentType); 212 outputMessage.getHeaders().setContentType(selectedContentType); 213 214 if (outputMessage instanceof StreamingHttpOutputMessage) { 215 StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage; 216 streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() { 217 @Override 218 public void writeTo(OutputStream outputStream) throws IOException { 219 writeInternal(image, selectedContentType, outputStream); 220 } 221 }); 222 } 223 else { 224 writeInternal(image, selectedContentType, outputMessage.getBody()); 225 } 226 } 227 228 private MediaType getContentType(MediaType contentType) { 229 if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) { 230 contentType = getDefaultContentType(); 231 } 232 Assert.notNull(contentType, "Could not select Content-Type. " + 233 "Please specify one through the 'defaultContentType' property."); 234 return contentType; 235 } 236 237 private void writeInternal(BufferedImage image, MediaType contentType, OutputStream body) 238 throws IOException, HttpMessageNotWritableException { 239 240 ImageOutputStream imageOutputStream = null; 241 ImageWriter imageWriter = null; 242 try { 243 Iterator<ImageWriter> imageWriters = ImageIO.getImageWritersByMIMEType(contentType.toString()); 244 if (imageWriters.hasNext()) { 245 imageWriter = imageWriters.next(); 246 ImageWriteParam iwp = imageWriter.getDefaultWriteParam(); 247 process(iwp); 248 imageOutputStream = createImageOutputStream(body); 249 imageWriter.setOutput(imageOutputStream); 250 imageWriter.write(null, new IIOImage(image, null, null), iwp); 251 } 252 else { 253 throw new HttpMessageNotWritableException( 254 "Could not find javax.imageio.ImageWriter for Content-Type [" + contentType + "]"); 255 } 256 } 257 finally { 258 if (imageWriter != null) { 259 imageWriter.dispose(); 260 } 261 if (imageOutputStream != null) { 262 try { 263 imageOutputStream.close(); 264 } 265 catch (IOException ex) { 266 // ignore 267 } 268 } 269 } 270 } 271 272 private ImageOutputStream createImageOutputStream(OutputStream os) throws IOException { 273 if (this.cacheDir != null) { 274 return new FileCacheImageOutputStream(os, this.cacheDir); 275 } 276 else { 277 return new MemoryCacheImageOutputStream(os); 278 } 279 } 280 281 282 /** 283 * Template method that allows for manipulating the {@link ImageReadParam} 284 * before it is used to read an image. 285 * <p>The default implementation is empty. 286 */ 287 protected void process(ImageReadParam irp) { 288 } 289 290 /** 291 * Template method that allows for manipulating the {@link ImageWriteParam} 292 * before it is used to write an image. 293 * <p>The default implementation is empty. 294 */ 295 protected void process(ImageWriteParam iwp) { 296 } 297 298}