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}