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}