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}