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.iiif;
20  
21  import java.awt.Graphics2D;
22  import java.awt.image.BufferedImage;
23  import java.io.IOException;
24  import java.nio.file.FileSystem;
25  import java.nio.file.FileSystemNotFoundException;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.util.Arrays;
29  import java.util.Locale;
30  import java.util.Map;
31  import java.util.Optional;
32  import java.util.stream.Collectors;
33  
34  import javax.imageio.ImageIO;
35  import javax.imageio.ImageReader;
36  
37  import org.apache.logging.log4j.LogManager;
38  import org.apache.logging.log4j.Logger;
39  import org.mycore.access.MCRAccessException;
40  import org.mycore.access.MCRAccessManager;
41  import org.mycore.common.config.MCRConfiguration2;
42  import org.mycore.common.config.MCRConfigurationException;
43  import org.mycore.iiif.image.MCRIIIFImageUtil;
44  import org.mycore.iiif.image.impl.MCRIIIFImageImpl;
45  import org.mycore.iiif.image.impl.MCRIIIFImageNotFoundException;
46  import org.mycore.iiif.image.impl.MCRIIIFImageProvidingException;
47  import org.mycore.iiif.image.impl.MCRIIIFUnsupportedFormatException;
48  import org.mycore.iiif.image.model.MCRIIIFFeatures;
49  import org.mycore.iiif.image.model.MCRIIIFImageInformation;
50  import org.mycore.iiif.image.model.MCRIIIFImageProfile;
51  import org.mycore.iiif.image.model.MCRIIIFImageQuality;
52  import org.mycore.iiif.image.model.MCRIIIFImageSourceRegion;
53  import org.mycore.iiif.image.model.MCRIIIFImageTargetRotation;
54  import org.mycore.iiif.image.model.MCRIIIFImageTargetSize;
55  import org.mycore.iiif.image.model.MCRIIIFImageTileInformation;
56  import org.mycore.iiif.model.MCRIIIFBase;
57  import org.mycore.imagetiler.MCRTiledPictureProps;
58  import org.mycore.iview2.backend.MCRDefaultTileFileProvider;
59  import org.mycore.iview2.backend.MCRTileFileProvider;
60  import org.mycore.iview2.backend.MCRTileInfo;
61  import org.mycore.iview2.services.MCRIView2Tools;
62  
63  public class MCRIVIEWIIIFImageImpl extends MCRIIIFImageImpl {
64  
65      public static final String DEFAULT_PROTOCOL = "http://iiif.io/api/image";
66  
67      public static final double LOG_HALF = Math.log(1.0 / 2.0);
68  
69      public static final java.util.List<String> SUPPORTED_FORMATS = Arrays.asList(ImageIO.getReaderFileSuffixes());
70  
71      public static final String MAX_BYTES_PROPERTY = "MaxImageBytes";
72  
73      private static final String TILE_FILE_PROVIDER_PROPERTY = "TileFileProvider";
74  
75      private static final String IDENTIFIER_SEPARATOR_PROPERTY = "IdentifierSeparator";
76  
77      private static final Logger LOGGER = LogManager.getLogger(MCRIVIEWIIIFImageImpl.class);
78  
79      private final java.util.List<String> transparentFormats;
80  
81      protected final MCRTileFileProvider tileFileProvider;
82  
83      public MCRIVIEWIIIFImageImpl(String implName) {
84          super(implName);
85          Map<String, String> properties = getProperties();
86          String tileFileProviderClassName = properties.get(TILE_FILE_PROVIDER_PROPERTY);
87          if (tileFileProviderClassName == null) {
88              tileFileProvider = new MCRDefaultTileFileProvider();
89          } else {
90              Optional<MCRTileFileProvider> optTFP = MCRConfiguration2
91                  .getInstanceOf(getConfigPrefix() + TILE_FILE_PROVIDER_PROPERTY);
92              if (optTFP.isPresent()) {
93                  tileFileProvider = optTFP.get();
94              } else {
95                  throw new MCRConfigurationException(
96                      "Configurated class (" + TILE_FILE_PROVIDER_PROPERTY + ") not found: "
97                          + tileFileProviderClassName);
98              }
99          }
100 
101         transparentFormats = Arrays.asList(properties.get("TransparentFormats").split(","));
102     }
103 
104     private String buildURL(String identifier) {
105         return MCRIIIFImageUtil.getIIIFURL(this) + MCRIIIFImageUtil.encodeImageIdentifier(identifier);
106     }
107 
108     @Override
109     public BufferedImage provide(String identifier,
110         MCRIIIFImageSourceRegion region,
111         MCRIIIFImageTargetSize targetSize,
112         MCRIIIFImageTargetRotation rotation,
113         MCRIIIFImageQuality imageQuality,
114         String format) throws MCRIIIFImageNotFoundException, MCRIIIFImageProvidingException,
115         MCRIIIFUnsupportedFormatException, MCRAccessException {
116 
117         long resultingSize = (long) targetSize.getHeight() * targetSize.getWidth()
118             * (imageQuality.equals(MCRIIIFImageQuality.color) ? 3 : 1);
119 
120         long maxImageSize = Optional.ofNullable(getProperties().get(MAX_BYTES_PROPERTY)).map(Long::parseLong)
121             .orElseThrow(() -> MCRConfiguration2.createConfigurationException(getConfigPrefix() + MAX_BYTES_PROPERTY));
122         if (resultingSize > maxImageSize) {
123             throw new MCRIIIFImageProvidingException("Maximal image size is " + (maxImageSize / 1024 / 1024) + "MB. ["
124                 + resultingSize + "/" + maxImageSize + "]");
125         }
126 
127         if (!SUPPORTED_FORMATS.contains(format.toLowerCase(Locale.ENGLISH))) {
128             throw new MCRIIIFUnsupportedFormatException(format);
129         }
130 
131         MCRTileInfo tileInfo = createTileInfo(identifier);
132         Optional<Path> oTileFile = tileFileProvider.getTileFile(tileInfo);
133         if (oTileFile.isEmpty()) {
134             throw new MCRIIIFImageNotFoundException(identifier);
135         }
136         checkTileFile(identifier, tileInfo, oTileFile.get());
137         MCRTiledPictureProps tiledPictureProps = getTiledPictureProps(oTileFile.get());
138 
139         int sourceWidth = region.getX2() - region.getX1();
140         int sourceHeight = region.getY2() - region.getY1();
141 
142         double targetWidth = targetSize.getWidth();
143         double targetHeight = targetSize.getHeight();
144 
145         double rotatationRadians = Math.toRadians(rotation.getDegrees());
146         double sinRotation = Math.sin(rotatationRadians);
147         double cosRotation = Math.cos(rotatationRadians);
148 
149         final int height = (int) (Math.abs(targetWidth * sinRotation) + Math.abs(targetHeight * cosRotation));
150         final int width = (int) (Math.abs(targetWidth * cosRotation) + Math.abs(targetHeight * sinRotation));
151 
152         BufferedImage targetImage;
153         switch (imageQuality) {
154         case bitonal:
155             targetImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY);
156             break;
157         case gray:
158             targetImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
159             break;
160         case color:
161         default:
162             if (transparentFormats.contains(format)) {
163                 targetImage = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
164             } else {
165                 targetImage = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
166             }
167         }
168 
169         // this value determines the zoom level!
170         double largestScaling = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);
171 
172         // We always want to use the the best needed zoom level!
173         int sourceZoomLevel = (int) Math.min(
174             Math.max(0, Math.ceil(tiledPictureProps.getZoomlevel() - Math.log(largestScaling) / LOG_HALF)),
175             tiledPictureProps.getZoomlevel());
176 
177         // largestScaling is the real scale which is needed! zoomLevelScale is the scale of the nearest zoom level!
178         double zoomLevelScale = Math.min(1.0, Math.pow(0.5, tiledPictureProps.getZoomlevel() - sourceZoomLevel));
179 
180         // this is the scale which is needed from the nearest zoom level to the required size of image
181         double drawScaleX = (targetWidth / (sourceWidth * zoomLevelScale)),
182             drawScaleY = (targetHeight / (sourceHeight * zoomLevelScale));
183 
184         // absolute region in zoom level this nearest zoom level
185         double x1 = region.getX1() * zoomLevelScale,
186             x2 = region.getX2() * zoomLevelScale,
187             y1 = region.getY1() * zoomLevelScale,
188             y2 = region.getY2() * zoomLevelScale;
189 
190         // now we detect the tiles to draw!
191         int x1Tile = (int) Math.floor(x1 / 256),
192             y1Tile = (int) Math.floor(y1 / 256),
193             x2Tile = (int) Math.ceil(x2 / 256),
194             y2Tile = (int) Math.ceil(y2 / 256);
195 
196         try (FileSystem zipFileSystem = MCRIView2Tools.getFileSystem(oTileFile.get())) {
197             Path rootPath = zipFileSystem.getPath("/");
198 
199             Graphics2D graphics = targetImage.createGraphics();
200             if (rotation.isMirrored()) {
201                 graphics.scale(-1, 1);
202                 graphics.translate(-width, 0);
203             }
204 
205             int xt = (int) ((targetWidth - 1) / 2), yt = (int) ((targetHeight - 1) / 2);
206             graphics.translate((width - targetWidth) / 2, (height - targetHeight) / 2);
207             graphics.rotate(rotatationRadians, xt, yt);
208 
209             graphics.scale(drawScaleX, drawScaleY);
210             graphics.translate(-x1, -y1);
211 
212             graphics.scale(zoomLevelScale, zoomLevelScale);
213             graphics.setClip(region.getX1(), region.getY1(), sourceWidth, sourceHeight);
214             graphics.scale(1 / zoomLevelScale, 1 / zoomLevelScale);
215 
216             LOGGER.info(String.format(Locale.ROOT, "Using zoom-level: %d and scales %s/%s!", sourceZoomLevel,
217                 drawScaleX, drawScaleY));
218 
219             for (int x = x1Tile; x < x2Tile; x++) {
220                 for (int y = y1Tile; y < y2Tile; y++) {
221                     ImageReader imageReader = MCRIView2Tools.getTileImageReader();
222                     BufferedImage tile = MCRIView2Tools.readTile(rootPath, imageReader, sourceZoomLevel, x, y);
223                     graphics.drawImage(tile, x * 256, y * 256, null);
224                 }
225             }
226 
227         } catch (IOException e) {
228             throw new MCRIIIFImageProvidingException("Error while reading tiles!", e);
229         }
230 
231         return targetImage;
232     }
233 
234     public MCRIIIFImageInformation getInformation(String identifier)
235         throws MCRIIIFImageNotFoundException, MCRIIIFImageProvidingException, MCRAccessException {
236         try {
237             MCRTileInfo tileInfo = createTileInfo(identifier);
238             Optional<Path> oTiledFile = tileFileProvider.getTileFile(tileInfo);
239             if (oTiledFile.isEmpty()) {
240                 throw new MCRIIIFImageNotFoundException(identifier);
241             }
242             final Path tileFilePath = oTiledFile.get();
243             checkTileFile(identifier, tileInfo, tileFilePath);
244             MCRTiledPictureProps tiledPictureProps = getTiledPictureProps(tileFilePath);
245 
246             MCRIIIFImageInformation imageInformation = new MCRIIIFImageInformation(MCRIIIFBase.API_IMAGE_2,
247                 buildURL(identifier), DEFAULT_PROTOCOL, tiledPictureProps.getWidth(), tiledPictureProps.getHeight(),
248                 Files.getLastModifiedTime(tileFilePath).toMillis());
249 
250             MCRIIIFImageTileInformation tileInformation = new MCRIIIFImageTileInformation(256, 256);
251             for (int i = 0; i < tiledPictureProps.getZoomlevel(); i++) {
252                 tileInformation.scaleFactors.add((int) Math.pow(2, i));
253             }
254 
255             imageInformation.tiles.add(tileInformation);
256 
257             return imageInformation;
258         } catch (FileSystemNotFoundException | IOException e) {
259             LOGGER.error("Could not find Iview ZIP for {}", identifier, e);
260             throw new MCRIIIFImageNotFoundException(identifier);
261         }
262     }
263 
264     @Override
265     public MCRIIIFImageProfile getProfile() {
266         MCRIIIFImageProfile mcriiifImageProfile = new MCRIIIFImageProfile();
267 
268         mcriiifImageProfile.formats = SUPPORTED_FORMATS.stream().filter(s -> !s.isEmpty())
269             .collect(Collectors.toSet());
270 
271         mcriiifImageProfile.qualities.add("color");
272         mcriiifImageProfile.qualities.add("bitonal");
273         mcriiifImageProfile.qualities.add("gray");
274 
275         mcriiifImageProfile.supports.add(MCRIIIFFeatures.mirroring);
276         mcriiifImageProfile.supports.add(MCRIIIFFeatures.regionByPct);
277         mcriiifImageProfile.supports.add(MCRIIIFFeatures.regionByPx);
278         mcriiifImageProfile.supports.add(MCRIIIFFeatures.rotationArbitrary);
279         mcriiifImageProfile.supports.add(MCRIIIFFeatures.rotationBy90s);
280         mcriiifImageProfile.supports.add(MCRIIIFFeatures.sizeAboveFull);
281         mcriiifImageProfile.supports.add(MCRIIIFFeatures.sizeByWhListed);
282         mcriiifImageProfile.supports.add(MCRIIIFFeatures.sizeByForcedWh);
283         mcriiifImageProfile.supports.add(MCRIIIFFeatures.sizeByH);
284         mcriiifImageProfile.supports.add(MCRIIIFFeatures.sizeByPct);
285         mcriiifImageProfile.supports.add(MCRIIIFFeatures.sizeByW);
286         mcriiifImageProfile.supports.add(MCRIIIFFeatures.sizeByWh);
287 
288         return mcriiifImageProfile;
289     }
290 
291     private MCRTiledPictureProps getTiledPictureProps(Path tiledFile) throws MCRIIIFImageProvidingException {
292         MCRTiledPictureProps tiledPictureProps = null;
293         try (FileSystem fileSystem = MCRIView2Tools.getFileSystem(tiledFile)) {
294             tiledPictureProps = MCRTiledPictureProps.getInstanceFromDirectory(fileSystem.getPath("/"));
295         } catch (IOException e) {
296             throw new MCRIIIFImageProvidingException("Could not provide image information!", e);
297         }
298         return tiledPictureProps;
299     }
300 
301     protected MCRTileInfo createTileInfo(String identifier) throws MCRIIIFImageNotFoundException {
302         MCRTileInfo tileInfo = null;
303         String id = identifier.contains(":/") ? identifier.replaceFirst(":/", "/") : identifier;
304         String separator = getProperties().getOrDefault(IDENTIFIER_SEPARATOR_PROPERTY, "/");
305         String[] splittedIdentifier = id.split(separator, 2);
306         switch (splittedIdentifier.length) {
307         case 1:
308             tileInfo = new MCRTileInfo(null, identifier, null);
309             break;
310         case 2:
311             tileInfo = new MCRTileInfo(splittedIdentifier[0], splittedIdentifier[1], null);
312             break;
313         default:
314             throw new MCRIIIFImageNotFoundException(identifier);
315         }
316         return tileInfo;
317     }
318 
319     private void checkTileFile(String identifier, MCRTileInfo tileInfo, Path tileFilePath)
320         throws MCRAccessException, MCRIIIFImageNotFoundException {
321         if (!Files.exists(tileFilePath)) {
322             throw new MCRIIIFImageNotFoundException(identifier);
323         }
324         if (tileInfo.getDerivate() != null
325             && !checkPermission(identifier, tileInfo)) {
326             throw MCRAccessException.missingPermission(
327                 "View the file " + tileInfo.getImagePath() + " in " + tileInfo.getDerivate(), tileInfo.getDerivate(),
328                 MCRAccessManager.PERMISSION_VIEW);
329         }
330     }
331 
332     protected boolean checkPermission(String identifier, MCRTileInfo tileInfo) {
333         return MCRAccessManager.checkPermission(tileInfo.getDerivate(), MCRAccessManager.PERMISSION_VIEW) ||
334             MCRAccessManager.checkPermission(tileInfo.getDerivate(), MCRAccessManager.PERMISSION_READ);
335     }
336 }