001/*
002 *  Copyright 2016 Anyware Services
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 *      http://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 */
016package org.ametys.core.resources;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.io.OutputStream;
021import java.io.Serializable;
022import java.util.Collection;
023import java.util.Map;
024import java.util.Set;
025import java.util.regex.Matcher;
026import java.util.regex.Pattern;
027
028import org.apache.avalon.framework.parameters.Parameters;
029import org.apache.cocoon.ProcessingException;
030import org.apache.cocoon.ResourceNotFoundException;
031import org.apache.commons.io.IOUtils;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.excalibur.source.Source;
034import org.apache.excalibur.source.SourceNotFoundException;
035import org.apache.excalibur.source.SourceResolver;
036
037import org.ametys.core.util.ImageHelper;
038
039/**
040 * Resource handler for images
041 */
042public class ImageResourceHandler extends SimpleResourceHandler
043{
044    private static final Pattern _SIZE_PATTERN = Pattern.compile("^(.+)_(max|crop|)(\\d+)x(\\d+)(\\.[^./]+)?$");
045
046    private static final Collection<String> __ALLOWED_OUTPUT_FORMATS = Set.of("png", "gif", "jpg", "jpeg");
047    private static final Collection<String> __UNRESIZABLE_FORMATS = Set.of("svg");
048    
049    private int _height;
050    private int _width;
051    private int _maxHeight;
052    private int _maxWidth;
053    private int _cropHeight;
054    private int _cropWidth;
055    
056    private boolean _download;
057    
058    record SizedSource(Source source, int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth) { /* empty */ }
059    
060    /**
061     * Default constructor
062     */
063    public ImageResourceHandler()
064    {
065        super();
066    }
067    
068    /**
069     * If the {@link Source} is already resolved by the {@link ResourceHandlerProvider},
070     * it may provide it through the constructor to avoid resolving it again.
071     * @param sizedSource the source.
072     */
073    public ImageResourceHandler(SizedSource sizedSource)
074    {
075        super(sizedSource.source);
076        _toFields(sizedSource);
077    }
078    
079    private void _toFields(SizedSource sizedSource)
080    {
081        _source = sizedSource.source;
082        _height = sizedSource.height;
083        _width = sizedSource.width;
084        _maxHeight = sizedSource.maxHeight;
085        _maxWidth = sizedSource.maxWidth;
086        _cropHeight = sizedSource.cropHeight;
087        _cropWidth = sizedSource.cropWidth;
088    }
089
090    @Override
091    public Source setup(String location, Map objectModel, Parameters par, boolean readForDownload) throws IOException, ProcessingException
092    {
093        if (_source == null)
094        {
095            // If the source has not been resolved by the provider, do it now
096            SizedSource sizedSource = _resolveSource(location, _resolver);
097            if (sizedSource != null)
098            {
099                _toFields(sizedSource);
100            }
101        }
102        
103        if (_source != null)
104        {
105            _download = readForDownload;
106            
107            return _source;
108        }
109        else
110        {
111            throw new ResourceNotFoundException("Resource not found for URI : " + location);
112        }
113    }
114
115    @Override
116    public void generate(OutputStream out) throws IOException, ProcessingException
117    {
118        String fileExtension = StringUtils.substringAfterLast(_source.getURI(), ".").toLowerCase();
119        
120        try (InputStream is = _source.getInputStream())
121        {
122            if (_processImage(fileExtension))
123            {
124                String outputFormat = __ALLOWED_OUTPUT_FORMATS.contains(fileExtension) ? fileExtension : "png";
125                ImageHelper.generateThumbnail(is, out, outputFormat, _height, _width, _maxHeight, _maxWidth, _cropHeight, _cropWidth);
126            }
127            else
128            {
129                // Copy data in response
130                IOUtils.copy(is, out);
131            }
132        }
133    }
134    
135    private boolean _processImage(String fileExtension)
136    {
137        if (__UNRESIZABLE_FORMATS.contains(fileExtension))
138        {
139            return false;
140        }
141        else if (_width > 0 || _height > 0 || _maxHeight > 0 || _maxWidth > 0 || _cropHeight > 0 || _cropWidth > 0)
142        {
143            // resize or crop is required, assume this is an image
144            return true;
145        }
146        else if (!_download)
147        {
148            String mimeType = _source.getMimeType();
149                    
150            // only process image if it is for rendering purposes
151            return mimeType != null && mimeType.startsWith("image/");
152        }
153        else
154        {
155            return false;
156        }
157    }
158
159    @Override
160    public Serializable getKey()
161    {
162        return _source.getURI() + "###" + _width + "x" + _height + "x" + _maxWidth + "x" + _maxHeight + "x" + _cropWidth + "x" + _cropHeight;
163    }
164    
165    /**
166     * Resolve the source at the given location
167     * @param location the location of the source to resolve
168     * @param resolver the source resolver
169     * @return the resolved source or null
170     */
171    static SizedSource _resolveSource(String location, SourceResolver resolver)
172    {
173        try
174        {
175            Source source = null;
176            try
177            {
178                source = resolver.resolveURI(location);
179                if (source != null && source.exists())
180                {
181                    return new SizedSource(source, 0, 0, 0, 0, 0, 0);
182                }
183                else
184                {
185                    throw new SourceNotFoundException(location);
186                }
187            }
188            catch (SourceNotFoundException e)
189            {
190                resolver.release(source);
191            }
192            
193            
194            Matcher sizeMatcher = _SIZE_PATTERN.matcher(location);
195            if (sizeMatcher.matches())
196            {
197                String computedLocation = sizeMatcher.group(1);
198                String suffix = sizeMatcher.group(5);
199                if (suffix != null)
200                {
201                    computedLocation += suffix;
202                }
203                
204                try
205                {
206                    source = resolver.resolveURI(computedLocation);
207                    if (!source.exists())
208                    {
209                        throw new SourceNotFoundException(computedLocation);
210                    }
211                }
212                catch (SourceNotFoundException e)
213                {
214                    resolver.release(source);
215                    return null;
216                }
217                
218                // type is either empty (resize), max or crop.
219                String type = sizeMatcher.group(2);
220                
221                int pHeight = Integer.parseInt(sizeMatcher.group(3));
222                int pWidth = Integer.parseInt(sizeMatcher.group(4));
223
224                int height = "".equals(type) ? pHeight : 0;
225                int width = "".equals(type) ? pWidth : 0;
226                int maxHeight = "max".equals(type) ? pHeight : 0;
227                int maxWidth = "max".equals(type) ? pWidth : 0;
228                int cropHeight = "crop".equals(type) ? pHeight : 0;
229                int cropWidth = "crop".equals(type) ? pWidth : 0;
230                
231                return new SizedSource(source, height, width, maxHeight, maxWidth, cropHeight, cropWidth);
232            }
233            else
234            {
235                return null;
236            }
237        }
238        catch (IOException e)
239        {
240            return null;
241        }
242    }
243}