001/*
002 *  Copyright 2010 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.skinfactory.readers;
017
018import java.awt.Dimension;
019import java.awt.image.BufferedImage;
020import java.io.ByteArrayInputStream;
021import java.io.File;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025import java.io.Serializable;
026import java.io.UnsupportedEncodingException;
027import java.net.URLDecoder;
028import java.util.Map;
029
030import javax.imageio.ImageIO;
031
032import org.apache.avalon.framework.parameters.Parameters;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.cocoon.ProcessingException;
036import org.apache.cocoon.caching.CacheableProcessingComponent;
037import org.apache.cocoon.environment.ObjectModelHelper;
038import org.apache.cocoon.environment.Response;
039import org.apache.cocoon.environment.SourceResolver;
040import org.apache.cocoon.reading.ServiceableReader;
041import org.apache.commons.io.IOUtils;
042import org.apache.commons.lang.StringUtils;
043import org.apache.excalibur.source.SourceValidity;
044import org.apache.excalibur.source.impl.FileSource;
045import org.xml.sax.SAXException;
046
047import org.ametys.core.util.ImageHelper;
048import org.ametys.plugins.skincommons.SkinEditionHelper;
049import org.ametys.web.skin.SkinModel;
050import org.ametys.web.skin.SkinModelsManager;
051
052import net.coobird.thumbnailator.makers.FixedSizeThumbnailMaker;
053import net.coobird.thumbnailator.resizers.DefaultResizerFactory;
054
055/**
056 * Reader for resource of the skin
057 */
058public class SkinResourceReader extends ServiceableReader implements CacheableProcessingComponent
059{
060    private SkinModelsManager _modelsManager;
061    private SkinEditionHelper _skinHelper;
062    
063    private FileSource _source;
064    
065    private int _width;
066    private int _height;
067    private int _maxWidth;
068    private int _maxHeight;
069    
070    @Override
071    public void service(ServiceManager sManager) throws ServiceException
072    {
073        super.service(sManager);
074        _modelsManager = (SkinModelsManager) sManager.lookup(SkinModelsManager.ROLE);
075        _skinHelper = (SkinEditionHelper) sManager.lookup(SkinEditionHelper.ROLE);
076    }
077    
078    @Override
079    public void setup(SourceResolver sResolver, Map objModel, String src, Parameters par) throws ProcessingException, SAXException, IOException
080    {
081        super.setup(sResolver, objModel, src, par);
082        
083        // parameters for image resizing
084        _width = par.getParameterAsInteger("width", 0);
085        _height = par.getParameterAsInteger("height", 0);
086        _maxWidth = par.getParameterAsInteger("maxWidth", 0);
087        _maxHeight = par.getParameterAsInteger("maxHeight", 0);
088        
089        String path = par.getParameter("path", null);
090        assert path != null;
091        
092        File rootDir = null;
093        String modelName = par.getParameter("modelName", null);
094        if (StringUtils.isNotEmpty(modelName))
095        {
096            SkinModel model = _modelsManager.getModel(modelName);
097            rootDir = model.getFile();
098        }
099        else
100        {
101            String skinName = par.getParameter("skinName", null);
102            rootDir = _skinHelper.getTempDirectory(skinName);
103        }
104        
105        _source = new FileSource("file", new File(rootDir, _decodePath(path)));
106    }
107    
108    /**
109     * Decode the resource path
110     * @param path the resource path
111     * @return the decoded resource path
112     * @throws UnsupportedEncodingException if UTF-8 encoding is not supported
113     */
114    protected String _decodePath (String path) throws UnsupportedEncodingException
115    {
116        StringBuffer sb = new StringBuffer();
117        
118        String[] parts = path.split("/");
119        for (String part : parts)
120        {
121            sb.append("/");
122            sb.append(URLDecoder.decode(part, "utf-8"));
123        }
124        return sb.toString();
125    }
126
127    @Override
128    public Serializable getKey()
129    {
130        return _source.getFile().getAbsolutePath() + "#" + _height + "#" + _width + "#" + _maxHeight + "#" + _maxWidth;
131    } 
132
133    @Override
134    public SourceValidity getValidity()
135    {
136        return _source.getValidity();
137    }
138    
139    @Override
140    public long getLastModified()
141    {
142        return _source.getLastModified();
143    }
144    
145    @Override
146    public String getMimeType()
147    {
148        return _source.getMimeType();
149    }
150    
151    @Override
152    public void generate() throws IOException, SAXException, ProcessingException
153    {
154        String name = _source.getName();
155        name = name.replaceAll("\\\\", "\\\\\\\\");
156        name = name.replaceAll("\\\"", "\\\\\\\"");
157        
158        Response response = ObjectModelHelper.getResponse(objectModel);
159        
160        try (InputStream is  = _source.getInputStream())
161        {
162            if (_width > 0 || _height > 0)
163            {
164                // it's an image, which must be resized
165                int i = name.lastIndexOf('.');
166                String format = name.substring(i + 1);
167                
168                ImageHelper.generateThumbnail(is, out, format, _height, _width, 0, 0);
169            }
170            else if (_maxHeight > 0 || _maxWidth > 0)
171            {
172                // it's an image, which must be resized
173                int i = name.lastIndexOf('.');
174                String format = name.substring(i + 1);
175                
176                byte[] fileContent = IOUtils.toByteArray(is);
177                BufferedImage src = ImageHelper.read(new ByteArrayInputStream(fileContent));
178                
179                _generateThumbnail (out, format, src, fileContent, _maxHeight, _maxWidth);
180            }
181            else
182            {
183                response.setHeader("Content-Length", Long.toString(_source.getContentLength()));
184                IOUtils.copy(is, out);
185            }
186        }
187        catch (Exception e)
188        {
189            throw new ProcessingException("Unable to download file of uri " + _source.getURI(), e);
190        }
191        finally
192        {
193            IOUtils.closeQuietly(out);
194        }
195    }
196    
197    @Override
198    public void recycle()
199    {
200        super.recycle();
201        _source = null;
202    }
203    
204    private void _generateThumbnail (OutputStream os, String format, BufferedImage src, byte[] fileContent, int maxHeight, int maxWidth) throws IOException
205    {
206        int srcHeight = src.getHeight();
207        int srcWidth = src.getWidth();
208        
209        if (maxWidth == srcWidth && maxHeight == srcHeight)
210        {
211            IOUtils.write(fileContent, os);
212            return;
213        }
214        if (srcWidth < maxWidth && srcHeight < maxHeight)
215        {
216            // Image is too small : zoom them crop image to minHeight x minWidth dimension
217            _generateZoomAndCropImage (out, format, src, fileContent, maxHeight, maxWidth);
218        }
219        else
220        {
221            BufferedImage dest = ImageHelper.generateThumbnail(src, _height, _width, _maxHeight, _maxWidth);
222            if (src == dest)
223            {
224                // Thumbnail is equals to src image, means that the image is the same
225                // We'd rather like return the initial stream
226                IOUtils.write(fileContent, os);
227            }
228            else
229            {
230                ImageIO.write(dest, format, os);
231            }
232        }
233    }
234    
235    private void _generateZoomAndCropImage (OutputStream os, String format,  BufferedImage src, byte[] fileContent, int minHeight, int minWidth) throws IOException
236    {
237        int srcHeight = src.getHeight();
238        int srcWidth = src.getWidth();
239        
240        int destHeight = 0;
241        int destWidth = 0;
242        
243        Dimension srcDimension = new Dimension(srcWidth, srcHeight);
244        
245        boolean keepAspectRatio = true;
246        
247        if (srcWidth > srcHeight)
248        {
249            destHeight = minHeight;
250            
251            // width is computed keeping ratio
252            destWidth = srcWidth * destHeight / srcHeight;
253        }
254        else
255        {
256            destWidth = minWidth;
257         
258            // dest is computed keeping ratio
259            destHeight = srcHeight * destWidth / srcWidth;
260        }
261        
262        Dimension thumbnailDimension = new Dimension(destWidth, destHeight);
263        
264        
265        BufferedImage thumbImage = new FixedSizeThumbnailMaker(destWidth, destHeight, keepAspectRatio, true)
266                                   .resizer(DefaultResizerFactory.getInstance().getResizer(srcDimension, thumbnailDimension)) 
267                                   .imageType(src.getColorModel().hasAlpha() ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB)
268                                   .make(src); 
269        
270        BufferedImage cropImage = _getCropImage (thumbImage, 0, 0, minHeight, minHeight);
271        
272        if (src == cropImage)
273        {
274            // Thumbnail is equals to src image, means that the image is the same
275            // We'd rather like return the initial stream
276            IOUtils.write(fileContent, os);
277        }
278        else
279        {
280            ImageIO.write(cropImage, format, os);
281        }
282    }
283    
284    private BufferedImage _getCropImage (BufferedImage src, int x, int y, int width, int height)
285    {
286        int srcHeight = src.getHeight();
287        int srcWidth = src.getWidth();
288        
289        int w = width;
290        if (width + x > srcWidth)
291        {
292            w = srcWidth - x;
293        }
294        
295        int h = height;
296        if (height + y > srcHeight)
297        {
298            h = srcHeight - y;
299        }
300        
301        return src.getSubimage(x, y, w, h);
302    }
303}