View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.iview2.frontend;
20  
21  import java.awt.Graphics2D;
22  import java.awt.RenderingHints;
23  import java.awt.image.BufferedImage;
24  import java.io.ByteArrayOutputStream;
25  import java.io.IOException;
26  import java.nio.file.FileSystem;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.nio.file.attribute.BasicFileAttributes;
30  import java.text.MessageFormat;
31  import java.util.Date;
32  import java.util.Locale;
33  import java.util.concurrent.ConcurrentLinkedQueue;
34  import java.util.concurrent.TimeUnit;
35  import java.util.concurrent.atomic.AtomicInteger;
36  
37  import javax.imageio.IIOImage;
38  import javax.imageio.ImageIO;
39  import javax.imageio.ImageReader;
40  import javax.imageio.ImageWriteParam;
41  import javax.imageio.ImageWriter;
42  import javax.imageio.stream.ImageOutputStream;
43  
44  import org.apache.logging.log4j.LogManager;
45  import org.apache.logging.log4j.Logger;
46  import org.jdom2.JDOMException;
47  import org.mycore.datamodel.niofs.MCRPathUtils;
48  import org.mycore.frontend.servlets.MCRServlet;
49  import org.mycore.frontend.servlets.MCRServletJob;
50  import org.mycore.imagetiler.MCRImage;
51  import org.mycore.imagetiler.MCRTiledPictureProps;
52  import org.mycore.iview2.services.MCRIView2Tools;
53  
54  import com.google.common.cache.CacheBuilder;
55  import com.google.common.cache.CacheLoader;
56  import com.google.common.cache.LoadingCache;
57  
58  import jakarta.servlet.ServletException;
59  import jakarta.servlet.ServletOutputStream;
60  import jakarta.servlet.http.HttpServletRequest;
61  import jakarta.servlet.http.HttpServletResponse;
62  
63  /**
64   * @author Thomas Scheffler (yagee)
65   *
66   */
67  public class MCRThumbnailServlet extends MCRServlet {
68      private static final long serialVersionUID = 1506443527774956290L;
69  
70      //stores max png size for byte array buffer of output
71      private AtomicInteger maxPngSize = new AtomicInteger(64 * 1024);
72  
73      private ImageWriteParam imageWriteParam;
74  
75      private ConcurrentLinkedQueue<ImageWriter> imageWriters = new ConcurrentLinkedQueue<>();
76  
77      private static Logger LOGGER = LogManager.getLogger(MCRThumbnailServlet.class);
78  
79      private int thumbnailSize = MCRImage.getTileSize();
80  
81      private static transient LoadingCache<String, Long> modifiedCache = CacheBuilder.newBuilder().maximumSize(5000)
82          .expireAfterWrite(MCRTileServlet.MAX_AGE, TimeUnit.SECONDS).weakKeys().build(new CacheLoader<String, Long>() {
83              @Override
84              public Long load(String id) throws Exception {
85                  ThumnailInfo thumbnailInfo = getThumbnailInfo(id);
86                  Path iviewFile = MCRImage.getTiledFile(MCRIView2Tools.getTileDir(), thumbnailInfo.derivate,
87                      thumbnailInfo.imagePath);
88                  try {
89                      return Files.readAttributes(iviewFile, BasicFileAttributes.class).lastModifiedTime().toMillis();
90                  } catch (IOException x) {
91                      return -1L;
92                  }
93              }
94          });
95  
96      @Override
97      public void init() throws ServletException {
98          super.init();
99          imageWriters = new ConcurrentLinkedQueue<>();
100         imageWriteParam = ImageIO.getImageWritersBySuffix("png").next().getDefaultWriteParam();
101         try {
102             imageWriteParam.setProgressiveMode(ImageWriteParam.MODE_DEFAULT);
103         } catch (UnsupportedOperationException e) {
104             LOGGER.warn("Your PNG encoder does not support progressive PNGs.");
105         }
106         String thSize = getInitParameter("thumbnailSize");
107         if (thSize != null) {
108             thumbnailSize = Integer.parseInt(thSize);
109         }
110         LOGGER.info("{}: setting thumbnail size to {}", getServletName(), thumbnailSize);
111     }
112 
113     @Override
114     public void destroy() {
115         for (ImageWriter imageWriter : imageWriters) {
116             imageWriter.dispose();
117         }
118         super.destroy();
119     }
120 
121     @Override
122     protected long getLastModified(HttpServletRequest request) {
123         return modifiedCache.getUnchecked(request.getPathInfo());
124     }
125 
126     @Override
127     protected void render(MCRServletJob job, Exception ex) throws IOException, JDOMException {
128         try {
129             ThumnailInfo thumbnailInfo = getThumbnailInfo(job.getRequest().getPathInfo());
130             Path iviewFile = MCRImage.getTiledFile(MCRIView2Tools.getTileDir(), thumbnailInfo.derivate,
131                 thumbnailInfo.imagePath);
132             LOGGER.info("IView2 file: {}", iviewFile);
133             BasicFileAttributes fileAttributes = MCRPathUtils.getAttributes(iviewFile, BasicFileAttributes.class);
134             if (fileAttributes == null) {
135                 job.getResponse().sendError(
136                     HttpServletResponse.SC_NOT_FOUND,
137                     new MessageFormat("Could not find iview2 file for {0}{1}", Locale.ROOT)
138                         .format(new Object[] { thumbnailInfo.derivate, thumbnailInfo.imagePath }));
139                 return;
140             }
141             String centerThumb = job.getRequest().getParameter("centerThumb");
142             //defaults to "yes"
143             boolean centered = !"no".equals(centerThumb);
144             BufferedImage thumbnail = getThumbnail(iviewFile, centered);
145 
146             if (thumbnail != null) {
147                 job.getResponse().setHeader("Cache-Control", "max-age=" + MCRTileServlet.MAX_AGE);
148                 job.getResponse().setContentType("image/png");
149                 job.getResponse().setDateHeader("Last-Modified", fileAttributes.lastModifiedTime().toMillis());
150                 Date expires = new Date(System.currentTimeMillis() + MCRTileServlet.MAX_AGE * 1000);
151                 LOGGER.debug("Last-Modified: {}, expire on: {}", fileAttributes.lastModifiedTime(), expires);
152                 job.getResponse().setDateHeader("Expires", expires.getTime());
153 
154                 ImageWriter imageWriter = getImageWriter();
155                 try (ServletOutputStream sout = job.getResponse().getOutputStream();
156                     ByteArrayOutputStream bout = new ByteArrayOutputStream(maxPngSize.get());
157                     ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(bout)) {
158                     imageWriter.setOutput(imageOutputStream);
159                     //tile = addWatermark(scaleBufferedImage(tile));        
160                     IIOImage iioImage = new IIOImage(thumbnail, null, null);
161                     imageWriter.write(null, iioImage, imageWriteParam);
162                     int contentLength = bout.size();
163                     maxPngSize.set(Math.max(maxPngSize.get(), contentLength));
164                     job.getResponse().setContentLength(contentLength);
165                     bout.writeTo(sout);
166                 } finally {
167                     imageWriter.reset();
168                     imageWriters.add(imageWriter);
169                 }
170             } else {
171                 job.getResponse().sendError(HttpServletResponse.SC_NOT_FOUND);
172             }
173         } finally {
174             LOGGER.debug("Finished sending {}", job.getRequest().getPathInfo());
175         }
176     }
177 
178     private static ThumnailInfo getThumbnailInfo(String pathInfo) {
179         String pInfo = pathInfo;
180         if (pInfo.startsWith("/")) {
181             pInfo = pInfo.substring(1);
182         }
183         final String derivate = pInfo.substring(0, pInfo.indexOf('/'));
184         String imagePath = pInfo.substring(derivate.length());
185         LOGGER.debug("derivate: {}, image: {}", derivate, imagePath);
186         return new ThumnailInfo(derivate, imagePath);
187     }
188 
189     private BufferedImage getThumbnail(Path iviewFile, boolean centered) throws IOException, JDOMException {
190         BufferedImage level1Image;
191         try (FileSystem fs = MCRIView2Tools.getFileSystem(iviewFile)) {
192             Path iviewFileRoot = fs.getRootDirectories().iterator().next();
193             MCRTiledPictureProps props = MCRTiledPictureProps.getInstanceFromDirectory(iviewFileRoot);
194             //get next bigger zoomLevel and scale image to THUMBNAIL_SIZE
195             ImageReader reader = MCRIView2Tools.getTileImageReader();
196             try {
197                 level1Image = MCRIView2Tools.getZoomLevel(iviewFileRoot, props, reader,
198                     Math.min(1, props.getZoomlevel()));
199             } finally {
200                 reader.dispose();
201             }
202         }
203         final double width = level1Image.getWidth();
204         final double height = level1Image.getHeight();
205         final int newWidth = width < height ? (int) Math.ceil(thumbnailSize * width / height) : thumbnailSize;
206         final int newHeight = width < height ? thumbnailSize : (int) Math.ceil(thumbnailSize * height / width);
207         //if centered make transparent image
208         int imageType = centered ? BufferedImage.TYPE_INT_ARGB : MCRImage.getImageType(level1Image);
209         //if centered make thumbnailSize x thumbnailSize image
210         final BufferedImage bicubic = new BufferedImage(centered ? thumbnailSize : newWidth, centered ? thumbnailSize
211             : newHeight, imageType);
212         final Graphics2D bg = bicubic.createGraphics();
213         try {
214             bg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
215             int x = centered ? (thumbnailSize - newWidth) / 2 : 0;
216             int y = centered ? (thumbnailSize - newHeight) / 2 : 0;
217             if (x != 0 && y != 0) {
218                 LOGGER.warn("Writing at position {},{}", x, y);
219             }
220             bg.drawImage(level1Image, x, y, x + newWidth, y + newHeight, 0, 0, (int) Math.ceil(width),
221                 (int) Math.ceil(height), null);
222         } finally {
223             bg.dispose();
224         }
225         return bicubic;
226     }
227 
228     private ImageWriter getImageWriter() {
229         ImageWriter imageWriter = imageWriters.poll();
230         if (imageWriter == null) {
231             imageWriter = ImageIO.getImageWritersBySuffix("png").next();
232         }
233         return imageWriter;
234     }
235 
236     private static class ThumnailInfo {
237         String derivate, imagePath;
238 
239         ThumnailInfo(final String derivate, final String imagePath) {
240             this.derivate = derivate;
241             this.imagePath = imagePath;
242         }
243 
244         @Override
245         public String toString() {
246             return "TileInfo [derivate=" + derivate + ", imagePath=" + imagePath + "]";
247         }
248     }
249 
250 }