001/*
002 * (C) Copyright 2006-2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nuxeo - initial API and implementation
016 *
017 */
018package org.nuxeo.ecm.platform.pictures.tiles.service;
019
020import java.io.File;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.Date;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030import org.nuxeo.common.utils.Path;
031import org.nuxeo.ecm.platform.commandline.executor.api.CommandException;
032import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable;
033import org.nuxeo.ecm.platform.picture.api.ImageInfo;
034import org.nuxeo.ecm.platform.picture.magick.utils.ImageIdentifier;
035import org.nuxeo.ecm.platform.picture.magick.utils.ImageResizer;
036import org.nuxeo.ecm.platform.pictures.tiles.api.PictureTiles;
037import org.nuxeo.ecm.platform.pictures.tiles.helpers.StringMaker;
038import org.nuxeo.runtime.api.Framework;
039
040/**
041 * Wraps the needed information about tiling a picture in order to manage cache. This includes:
042 * <ul>
043 * <li>original image stored on file system</li>
044 * <li>reduced images if any</li>
045 * <li>tiles already generated</li>
046 * </ul>
047 *
048 * @author tiry
049 */
050public class PictureTilingCacheInfo {
051
052    public static int SHRINK_DOWN_LIMIT_PX = 2000;
053
054    private static final Log log = LogFactory.getLog(PictureTilingCacheInfo.class);
055
056    protected String cacheKey;
057
058    protected String workingDir;
059
060    protected ImageInfo originalPictureInfos;
061
062    protected Map<Integer, ImageInfo> shrinkedImages;
063
064    protected List<Integer> shrinkedImagesWidths;
065
066    protected Map<String, PictureTiles> tilesSet;
067
068    protected String syncShrink = "oneOncePerInstance";
069
070    protected Date lastAccessTime;
071
072    protected void updateAccessTime() {
073        lastAccessTime = new Date();
074    }
075
076    public Date getLastAccessedTime() {
077        return lastAccessTime;
078    }
079
080    protected long getFileSize(String path) {
081        if (path == null)
082            return 0;
083        File file = new File(path);
084        if (file.exists()) {
085            return file.length();
086        } else
087            return 0;
088    }
089
090    public long getDiskSpaceUsageInBytes() {
091        long diskSpaceUsage = 0;
092
093        // original picture
094        diskSpaceUsage += getFileSize(originalPictureInfos.getFilePath());
095
096        // shrinked ones
097        for (Integer s : shrinkedImages.keySet()) {
098            diskSpaceUsage += getFileSize(shrinkedImages.get(s).getFilePath());
099        }
100
101        // tiles
102        for (String tileDef : tilesSet.keySet()) {
103            PictureTiles tiles = tilesSet.get(tileDef);
104            File tileDir = new File(tiles.getTilesPath());
105            if (tileDir.exists()) {
106                for (File tileFile : tileDir.listFiles()) {
107                    diskSpaceUsage += tileFile.length();
108                }
109            }
110        }
111
112        return diskSpaceUsage;
113    }
114
115    public PictureTilingCacheInfo(String cacheKey, String workingDir, String filePath) throws CommandNotAvailable,
116            CommandException {
117        this.cacheKey = cacheKey;
118        this.workingDir = workingDir;
119        originalPictureInfos = ImageIdentifier.getInfo(filePath);
120        shrinkedImages = new HashMap<>();
121        shrinkedImagesWidths = new ArrayList<>();
122        tilesSet = new HashMap<>();
123        updateAccessTime();
124    }
125
126    public void addPictureTilesToCache(PictureTiles tiles) {
127        tilesSet.put(tiles.getTileFormatCacheKey(), tiles);
128        updateAccessTime();
129    }
130
131    public PictureTiles getCachedPictureTiles(int tileWidth, int tileHeight, int maxTiles) {
132        String ptKey = StringMaker.getTileFormatString(tileWidth, tileHeight, maxTiles);
133        updateAccessTime();
134        return tilesSet.get(ptKey);
135    }
136
137    public String getWorkingDir() {
138        return workingDir;
139    }
140
141    public String getOriginalPicturePath() {
142        return originalPictureInfos.getFilePath();
143    }
144
145    public String getTilingDir(int tileWidth, int tileHeight, int maxTiles) {
146        String dirPath = "tiles-" + tileWidth + "-" + tileHeight + "-" + maxTiles;
147        dirPath = new Path(workingDir).append(dirPath).toString();
148
149        log.debug("Target tiling dir=" + dirPath);
150        File dir = new File(dirPath);
151
152        if (!dir.exists()) {
153            dir.mkdir();
154            Framework.trackFile(dir, this);
155        }
156        return dirPath;
157    }
158
159    public ImageInfo getBestSourceImage(int tileWidth, int tileHeight, int maxTiles) {
160        updateAccessTime();
161        if ("JPEG".equals(originalPictureInfos.getFormat())) {
162            // since JPEG supports it we may strip it down
163
164            if ((originalPictureInfos.getHeight() > SHRINK_DOWN_LIMIT_PX)
165                    || (originalPictureInfos.getWidth() > SHRINK_DOWN_LIMIT_PX)) {
166                int neededWidth = tileWidth * maxTiles;
167                int neededHeight = tileHeight * maxTiles;
168                int shrinkedWidth = 0;
169
170                // JPG simplification work with 2 factor
171                if ((neededHeight > (originalPictureInfos.getHeight() / 2))
172                        || (neededWidth > (originalPictureInfos.getWidth() / 2))) {
173                    return originalPictureInfos;
174                }
175
176                // avoid multiple shrink processing of the same image
177                synchronized (syncShrink) {
178                    for (Integer swidth : shrinkedImagesWidths) {
179                        if (swidth >= neededWidth)
180                            shrinkedWidth = swidth;
181                        else
182                            break;
183                    }
184
185                    if (shrinkedWidth > 0) {
186                        return shrinkedImages.get(new Integer(shrinkedWidth));
187                    } else {
188                        String shrinkedImagePath = new Path(workingDir).append(
189                                "reduced-" + neededWidth + "x" + neededHeight + ".jpg").toString();
190                        try {
191                            ImageInfo shrinked = ImageResizer.resize(originalPictureInfos.getFilePath(),
192                                    shrinkedImagePath, neededWidth, neededHeight, -1);
193
194                            shrinkedImagesWidths.add(new Integer(shrinked.getWidth()));
195                            Collections.sort(shrinkedImagesWidths);
196                            Collections.reverse(shrinkedImagesWidths);
197
198                            shrinkedImages.put(new Integer(shrinked.getWidth()), shrinked);
199
200                            return shrinked;
201                        } catch (CommandNotAvailable | CommandException e) {
202                            return originalPictureInfos;
203                        }
204                    }
205                }
206            } else
207                return originalPictureInfos;
208        } else
209            return originalPictureInfos;
210    }
211
212    public ImageInfo getOriginalPictureInfos() {
213        updateAccessTime();
214        return originalPictureInfos;
215    }
216
217    public void cleanUp() {
218        // original picture
219        File orgFile = new File(originalPictureInfos.getFilePath());
220        if (orgFile.exists())
221            orgFile.delete();
222
223        // shrinked ones
224        for (Integer s : shrinkedImages.keySet()) {
225            File skFile = new File(shrinkedImages.get(s).getFilePath());
226            if (skFile.exists())
227                skFile.delete();
228        }
229
230        // tiles
231        for (String tileDef : tilesSet.keySet()) {
232            PictureTiles tiles = tilesSet.get(tileDef);
233            File tileDir = new File(tiles.getTilesPath());
234            if (tileDir.exists()) {
235                for (File tileFile : tileDir.listFiles()) {
236                    tileFile.delete();
237                }
238            }
239        }
240    }
241
242    public void partialCleanUp(long targetDeltaInKB) {
243        long deletedKB = 0;
244        // tiles
245        for (String tileDef : tilesSet.keySet()) {
246            PictureTiles tiles = tilesSet.get(tileDef);
247            File tileDir = new File(tiles.getTilesPath());
248            if (tileDir.exists()) {
249                for (File tileFile : tileDir.listFiles()) {
250                    deletedKB += tileFile.length() / 1000;
251                    tileFile.delete();
252                    if (deletedKB > targetDeltaInKB)
253                        return;
254                }
255            }
256        }
257
258        // shrinked ones
259        for (Integer s : shrinkedImages.keySet()) {
260            File skFile = new File(shrinkedImages.get(s).getFilePath());
261            if (skFile.exists()) {
262                deletedKB += skFile.length() / 1000;
263                skFile.delete();
264                if (deletedKB > targetDeltaInKB)
265                    return;
266            }
267        }
268
269        // original picture
270        File orgFile = new File(originalPictureInfos.getFilePath());
271        if (orgFile.exists())
272            orgFile.delete();
273
274    }
275
276}