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