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