1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
65
66
67 public class MCRThumbnailServlet extends MCRServlet {
68 private static final long serialVersionUID = 1506443527774956290L;
69
70
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
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
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
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
208 int imageType = centered ? BufferedImage.TYPE_INT_ARGB : MCRImage.getImageType(level1Image);
209
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 }