001/*
002 *  Copyright 2019 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.plugins.core.upload.image;
017
018import java.awt.image.BufferedImage;
019import java.io.ByteArrayInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.util.Map;
024
025import javax.imageio.ImageIO;
026
027import org.apache.avalon.framework.parameters.ParameterException;
028import org.apache.avalon.framework.parameters.Parameters;
029import org.apache.cocoon.environment.Request;
030import org.apache.commons.io.FilenameUtils;
031
032import org.ametys.core.upload.Upload;
033import org.ametys.core.upload.UploadManager;
034import org.ametys.core.util.ImageHelper;
035import org.ametys.plugins.core.upload.UploadAction;
036
037/**
038 * Generates a new image by cropping an already uploaded image and store it.
039 */
040public class CropImageAction extends UploadAction
041{
042    @Override
043    protected void _doUpload(Request request, Parameters parameters, Map<String, Object> result) throws Exception
044    {
045        Upload originalImage = _getUploadObject(request, parameters);
046        Cropping cropping = new Cropping(parameters);
047        _doCrop(originalImage, cropping, result);
048    }
049    
050    /**
051     * Gets the already uploaded image
052     * @param request The request
053     * @param parameters The parameters
054     * @return The previously uploaded image
055     * @throws ParameterException if the <code>imageId</code> parameter is not specified
056     */
057    protected Upload _getUploadObject(Request request, Parameters parameters) throws ParameterException
058    {
059        String imageId = parameters.getParameter("imageId");
060        return _uploadManager.getUpload(_getCurrentUser(), imageId);
061    }
062    
063    /**
064     * Do crop the original image and fill the result map.
065     * @param originalImage The original image to crop
066     * @param cropping The cropping to apply
067     * @param result The result map to fill
068     * @throws IOException if an I/O error occurs
069     */
070    protected void _doCrop(Upload originalImage, Cropping cropping, Map<String, Object> result) throws IOException
071    {
072        if (cropping.isOriginal())
073        {
074            // Avoid an unnecessary cropping and the creation of a new image which will be the same as the original.
075            _fillSuccess(originalImage, result);
076            return;
077        }
078        
079        try (InputStream is = originalImage.getInputStream())
080        {
081            BufferedImage src = _uploadAsBufferedImage(originalImage);
082            int originalWidth = src.getWidth();
083            int originalHeight = src.getHeight();
084            
085            int x = cropping.getX(originalWidth);
086            int y = cropping.getY(originalHeight);
087            
088            int width = cropping.getWidth(originalWidth);
089            width = _checkValidity(width, x, originalWidth);
090            int height = cropping.getHeight(originalHeight);
091            height = _checkValidity(height, y, originalHeight);
092            
093            BufferedImage croppedImage = ImageHelper.generateCroppedImage(src, x, y, height, width);
094            _storeCroppedImage(croppedImage, originalImage.getFilename(), result);
095        }
096    }
097    
098    private int _checkValidity(int widthOrHeight, int xOrY, int originalWidthOrHeight)
099    {
100        if (xOrY + widthOrHeight > originalWidthOrHeight)
101        {
102            // It somehow overflows the original size, do return the maximum valid width/height
103            return originalWidthOrHeight - xOrY;
104        }
105        else
106        {
107            return widthOrHeight;
108        }
109    }
110    
111    /**
112     * Converts an uploaded image as {@link Upload} into a {@link BufferedImage}
113     * @param upload The uploaded image
114     * @return The upload image as a {@link BufferedImage}
115     * @throws IOException if an I/O error occurs
116     */
117    protected BufferedImage _uploadAsBufferedImage(Upload upload) throws IOException
118    {
119        try (InputStream is = upload.getInputStream())
120        {
121            return ImageIO.read(is);
122        }
123    }
124    
125    /**
126     * Stores the cropped image into the {@link UploadManager} system and fill the result map.
127     * @param croppedImage The cropped image
128     * @param originalName The original image file name
129     * @param result The result map to fill
130     * @throws IOException if an I/O error occurs
131     */
132    protected void _storeCroppedImage(BufferedImage croppedImage, String originalName, Map<String, Object> result) throws IOException
133    {
134        try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
135        {
136            ImageIO.write(croppedImage, _getFormatName(originalName), baos);
137            byte[] buffer = baos.toByteArray();
138            try (InputStream is = new ByteArrayInputStream(buffer))
139            {
140                _storeUpload(is, _getCroppedImageFilename(originalName), result);
141            }
142            catch (IOException e)
143            {
144                _handleStoreUploadException(e, croppedImage, result);
145            }
146        }
147    }
148    
149    /**
150     * Gets the String containing the informal name of the format of the image
151     * @param iamgeFilename The file name 
152     * @return the String containing the informal name of the format of the image
153     */
154    protected String _getFormatName(String iamgeFilename)
155    {
156        String format = FilenameUtils.getExtension(iamgeFilename);
157        return format.isEmpty() ? "png" : format;
158    }
159    
160    /**
161     * Gets the file name of the cropped image
162     * @param originalName The file name of the original image
163     * @return the file name of the cropped image
164     */
165    protected String _getCroppedImageFilename(String originalName)
166    {
167        // By default keep name of the original image file
168        return originalName;
169    }
170    
171    /**
172     * Represents a rectangular cropping of an image.
173     * <br>It stores only relative float values (which are between 0 and 1) as ratio of the width and height of the original image, which are unknown.
174     */
175    protected static class Cropping
176    {
177        private float _relativeX1;
178        private float _relativeY1;
179        private float _relativeWidth;
180        private float _relativeHeight;
181        
182        /**
183         * Builds a new Cropping object from {@link Parameters}
184         * @param parameters The parameters
185         */
186        protected Cropping(Parameters parameters)
187        {
188            _relativeX1 = parameters.getParameterAsFloat("x1", 0.0f);
189            _relativeY1 = parameters.getParameterAsFloat("y1", 0.0f);
190            _relativeWidth = parameters.getParameterAsFloat("width", 1.0f);
191            _relativeHeight = parameters.getParameterAsFloat("height", 1.0f);
192            
193            assert _relativeX1 >= 0;
194            assert _relativeX1 <= 1;
195            
196            assert _relativeY1 >= 0;
197            assert _relativeY1 <= 1;
198            
199            assert _relativeWidth >= 0;
200            assert _relativeWidth <= 1;
201            
202            assert _relativeHeight >= 0;
203            assert _relativeHeight <= 1;
204        }
205        
206        /**
207         * Returns <code>true</code> if this cropping actually represents the original image
208         * @return <code>true</code> if this cropping actually represents the original image
209         */
210        protected boolean isOriginal()
211        {
212            return _relativeX1 <= 0.0f && _relativeY1 <= 0.0f
213                && _relativeWidth >= 1.0f && _relativeHeight >= 1.0f;
214        }
215        
216        /**
217         * Gets the X coordinate of the upper-left corner of the cropping rectangular region
218         * @param originalWidth The width of the original image
219         * @return the X coordinate of the upper-left corner of the cropping rectangular region
220         */
221        protected int getX(int originalWidth)
222        {
223            return Math.round(originalWidth * _relativeX1);
224        }
225        
226        /**
227         * Gets the Y coordinate of the upper-left corner of the cropping rectangular region
228         * @param originalHeight The height of the original image
229         * @return the Y coordinate of the upper-left corner of the cropping rectangular region
230         */
231        protected int getY(int originalHeight)
232        {
233            return Math.round(originalHeight * _relativeY1);
234        }
235        
236        /**
237         * Gets the width of the cropping rectangular region
238         * @param originalWidth The width of the original image
239         * @return the width of the cropping rectangular region
240         */
241        protected int getWidth(int originalWidth)
242        {
243            return Math.round(originalWidth * _relativeWidth);
244        }
245        
246        /**
247         * Gets the height of the cropping rectangular region
248         * @param originalHeight The height of the original image
249         * @return the height of the cropping rectangular region
250         */
251        protected int getHeight(int originalHeight)
252        {
253            return Math.round(originalHeight * _relativeHeight);
254        }
255    }
256}