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