From e7f940b8fb64f5030590cd59b8207c297def1408 Mon Sep 17 00:00:00 2001 From: jensp Date: Tue, 25 Jul 2017 19:01:07 +0000 Subject: [PATCH] CCM NG/ccm-cms: JAX-RS based service for serving images. git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@4887 8810af33-2d31-482b-a856-94f89814c4df --- .../src/main/resources/log4j2.xml | 5 +- .../librecms/contentsection/rs/Images.java | 307 +++++++++++++++++- 2 files changed, 303 insertions(+), 9 deletions(-) diff --git a/ccm-bundle-devel-wildfly-web/src/main/resources/log4j2.xml b/ccm-bundle-devel-wildfly-web/src/main/resources/log4j2.xml index 3c20db8e4..9c55f1436 100644 --- a/ccm-bundle-devel-wildfly-web/src/main/resources/log4j2.xml +++ b/ccm-bundle-devel-wildfly-web/src/main/resources/log4j2.xml @@ -89,8 +89,11 @@ + + + level="debug"> \ No newline at end of file diff --git a/ccm-cms/src/main/java/org/librecms/contentsection/rs/Images.java b/ccm-cms/src/main/java/org/librecms/contentsection/rs/Images.java index 9cf49d7ce..83ac205b5 100644 --- a/ccm-cms/src/main/java/org/librecms/contentsection/rs/Images.java +++ b/ccm-cms/src/main/java/org/librecms/contentsection/rs/Images.java @@ -18,19 +18,32 @@ */ package org.librecms.contentsection.rs; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.librecms.assets.Image; import org.librecms.contentsection.Asset; import org.librecms.contentsection.AssetRepository; import org.librecms.contentsection.ContentSection; import org.librecms.contentsection.ContentSectionRepository; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; import java.util.Optional; import javax.enterprise.context.RequestScoped; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; import javax.inject.Inject; +import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; /** @@ -41,17 +54,55 @@ import javax.ws.rs.core.Response; @Path("/{content-section}/images/") public class Images { + private static final Logger LOGGER = LogManager.getLogger(Images.class); + @Inject private ContentSectionRepository sectionRepo; @Inject private AssetRepository assetRepo; + /** + * Return the image requested by the provided content section and path. + * + * The URL for an image contains the content section and the path to the + * image. If there is no image for the provided content section and path an + * 404 error is returned. + * + * This method also accepts two parameters which can be specified as query + * parameters on the URL: {@code width} and {@code height}. If one or both + * are provided the image is scaled before it is send the the user agent + * which requested the image. The method preserves the aspect ratio of the + * image. If the parameters have different scale factors meaning that the + * aspect ratio of the image would not be preserved the parameter with the + * smallest difference to one if used. The other parameter is ignored and + * replaced with a value which preserves the aspect ratio. + * + * @param sectionName The name of the content section which contains the + * image. + * @param path The path to the image. + * @param widthParam The width to scale the image. If the value is 0 or + * less or the value is not a valid integer it parameter + * is ignored. + * @param heightParam The height to scale the image. If the value is 0 or + * less or the value is not a valid integer it parameter + * is ignored. + * + * @return A {@link Response} containing the scaled image or an error value. + */ @GET @Path("/{path:.+}") public Response getImage( - @PathParam("content-section") final String sectionName, - @PathParam("path") final String path) { + @PathParam("content-section") + final String sectionName, + @PathParam("path") + final String path, + @QueryParam("width") + @DefaultValue("-1") + final String widthParam, + @QueryParam("height") + @DefaultValue("-1") + final String heightParam) { final Optional section = sectionRepo .findByLabel(sectionName); @@ -63,20 +114,84 @@ public class Images { .build(); } - final Optional asset = assetRepo.findByPath(section.get(), + final Optional asset = assetRepo.findByPath(section.get(), path); if (asset.isPresent()) { if (asset.get() instanceof Image) { final Image image = (Image) asset.get(); final byte[] data = image.getData(); + final String mimeType = image.getMimeType().toString(); + final InputStream inputStream = new ByteArrayInputStream(data); + final BufferedImage bufferedImage; + final String imageFormat; + try { + final ImageInputStream imageInputStream = ImageIO + .createImageInputStream(inputStream); + final Iterator readers = ImageIO + .getImageReaders(imageInputStream); + final ImageReader imageReader; + if (readers.hasNext()) { + imageReader = readers.next(); + } else { + LOGGER.error("No image reader for image {} (UUID: {}) " + + "available.", + image.getDisplayName(), + image.getUuid()); + return Response.serverError().build(); + } + imageReader.setInput(imageInputStream); + bufferedImage = imageReader.read(0); + imageFormat = imageReader.getFormatName(); + } catch (IOException ex) { + LOGGER.error("Failed to load image {} (UUID: {}).", + image.getDisplayName(), + image.getUuid()); + LOGGER.error(ex); + return Response.serverError().build(); + } + + // Yes, this is correct. The parameters provided in the URL + // are expected to be integers. The private scaleImage method + // works with floats to be accurate (divisions are performed + // with the values for width and height) + final int width = parseScaleParameter(widthParam, "width"); + final int height = parseScaleParameter(heightParam, "height"); + final java.awt.Image scaledImage = scaleImage(bufferedImage, + width, + height); + + final ByteArrayOutputStream outputStream + = new ByteArrayOutputStream(); + final BufferedImage bufferedScaledImage = new BufferedImage( + scaledImage.getWidth(null), + scaledImage.getHeight(null), + bufferedImage.getType()); + bufferedScaledImage + .getGraphics() + .drawImage(scaledImage, 0, 0, null); + try { + ImageIO + .write(bufferedScaledImage, imageFormat, outputStream); + } catch (IOException ex) { + LOGGER.error("Failed to render scaled variant of image {} " + + "(UUID: {}).", + image.getDisplayName(), + image.getUuid()); + LOGGER.error(ex); + return Response.serverError().build(); + } + +// return Response + // .ok(String.format( + // "Requested image \"%s\" in content section \"%s\"", + // path, + // section.get().getLabel()), + // "text/plain") + // .build(); return Response - .ok(String.format( - "Requested image \"%s\" in content section \"%s\"", - path, - section.get().getLabel()), - "text/plain") + .ok(outputStream.toByteArray(), mimeType) .build(); } else { return Response @@ -107,4 +222,180 @@ public class Images { // return builder.build(); } + /** + * Provides several properties of an image to a user agent as JSON. + * + * @param sectionName The name of the content section which contains the + * image. + * @param path The path to the image. + * + * @return A {@link Response} with the informations about the requested + * image. + */ + @GET + @Path("/{path:.*}/properties") + public Response getImageProperties( + @PathParam("content-section") final String sectionName, + @PathParam("path") final String path) { + + final Optional section = sectionRepo + .findByLabel(sectionName); + if (!section.isPresent()) { + return Response + .status(Response.Status.NOT_FOUND) + .entity(String.format("No content section \"%s\" available.", + sectionName)) + .build(); + } + + final Optional asset = assetRepo.findByPath(section.get(), + path); + + if (asset.isPresent()) { + if (asset.get() instanceof Image) { + final Image image = (Image) asset.get(); + final byte[] data = image.getData(); + final String mimeType = image.getMimeType().toString(); + + final InputStream inputStream = new ByteArrayInputStream(data); + final BufferedImage bufferedImage; + try { + bufferedImage = ImageIO.read(inputStream); + } catch (IOException ex) { + LOGGER.error("Failed to load image {} (UUID: {}).", + image.getDisplayName(), + image.getUuid()); + LOGGER.error(ex); + return Response.serverError().build(); + } + + final String imageProperties = String + .format("{%n" + + " \"name\": \"%s\",%n" + + " \"filename\": \"%s\",%n" + + " \"mimetype\": \"%s\",%n" + + " \"width\": %d,%n" + + " \"height\": %d%n" + + "}", + image.getDisplayName(), + image.getFileName(), + mimeType, + bufferedImage.getWidth(), + bufferedImage.getHeight()); + + return Response + .ok(imageProperties, "application/json") + .build(); + } else { + return Response + .status(Response.Status.NOT_FOUND) + .entity(String + .format("The asset found at the requested path \"%s\" " + + "is not an image.", + path)) + .build(); + } + } else { + return Response + .status(Response.Status.NOT_FOUND) + .entity(String + .format("The requested image \"%s\" does not exist.", + path)) + .build(); + } + } + + /** + * Helper method for parsing the parameters for scaling an image into + * integers. + * + * @param parameterValue The value to parse as integer. + * @param parameter The name of the parameter (used for logging + * output). + * + * @return The integer value of the parameter or -1 if the provided value is + * not a valid integer. + */ + private int parseScaleParameter(final String parameterValue, + final String parameter) { + try { + return Integer.parseInt(parameterValue); + } catch (NumberFormatException ex) { + LOGGER.warn("Provided value \"{}\" for parameter \"{}\" is " + + "not an integer. Ignoring value.", + parameterValue, + parameter); + LOGGER.warn(ex); + return -1; + } + } + + /** + * Helper method for scaling the image while preserving the aspect ratio of + * the image. + * + * @param image The image to scale. + * @param scaleToWidth The width to which is scaled. + * @param scaleToHeight The height the which image is scaled. + * + * @return The scaled image. + */ + private java.awt.Image scaleImage(final BufferedImage image, + final float scaleToWidth, + final float scaleToHeight) { + + final float originalWidth = image.getWidth(); + final float originalHeight = image.getHeight(); + final float originalAspectRatio = originalWidth / originalHeight; + + if (scaleToWidth > 0 && scaleToHeight > 0) { + //Check if parameters preserve aspectRatio. If not use the smaller + //scale factor. + + final float scaleToAspectRatio = scaleToWidth / scaleToHeight; + if (Math.abs(scaleToAspectRatio - originalAspectRatio) < 0.009f) { + // Scale the image. + + return image.getScaledInstance(Math.round(scaleToWidth), + Math.round(scaleToHeight), + java.awt.Image.SCALE_SMOOTH); + } else { + //Use the scale factor nearer to one for both dimensions + final float scaleFactorWidth = scaleToWidth / originalWidth; + final float scaleFactorHeight = scaleToHeight / originalHeight; + final float differenceWidth = Math.abs(scaleFactorWidth - 1); + final float differenceHeight = Math.abs(scaleFactorHeight - 1); + + final float scaleFactor; + if (differenceWidth < differenceHeight) { + scaleFactor = scaleFactorWidth; + } else { + scaleFactor = scaleFactorHeight; + } + + return scaleImage(image, + originalWidth * scaleFactor, + originalHeight * scaleFactor); + } + + } else if (scaleToWidth > 0 && scaleToHeight <= 0) { + //Calculate the height to which to image is scaled based on the + //scale factor for the width + final float scaleFactor = scaleToWidth / originalWidth; + final float height = originalHeight * scaleFactor; + + return scaleImage(image, scaleToWidth, height); + } else if (scaleToWidth <= 0 && scaleToHeight >= 0) { + //Calculate the width to which to image is scaled based on the + //scale factor for the height + final float scaleFactor = scaleToHeight / originalHeight; + final float width = originalWidth * scaleFactor; + + return scaleImage(image, width, scaleToHeight); + } else { + //Return the image as is. + return image; + } + } + }