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