diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java index 7b082c6c21..483e4f5f6e 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java @@ -7,9 +7,7 @@ */ package org.dspace.app.mediafilter; -import java.awt.image.BufferedImage; import java.io.InputStream; -import javax.imageio.ImageIO; import org.dspace.content.Item; import org.dspace.services.ConfigurationService; @@ -63,27 +61,20 @@ public class BrandedPreviewJPEGFilter extends MediaFilter { @Override public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) throws Exception { - // read in bitstream's image - BufferedImage buf = ImageIO.read(source); - // get config params ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - float xmax = (float) configurationService - .getIntProperty("webui.preview.maxwidth"); - float ymax = (float) configurationService - .getIntProperty("webui.preview.maxheight"); - boolean blurring = (boolean) configurationService - .getBooleanProperty("webui.preview.blurring"); - boolean hqscaling = (boolean) configurationService - .getBooleanProperty("webui.preview.hqscaling"); + int xmax = configurationService.getIntProperty("webui.preview.maxwidth"); + int ymax = configurationService.getIntProperty("webui.preview.maxheight"); + boolean blurring = configurationService.getBooleanProperty("webui.preview.blurring"); + boolean hqscaling = configurationService.getBooleanProperty("webui.preview.hqscaling"); int brandHeight = configurationService.getIntProperty("webui.preview.brand.height"); String brandFont = configurationService.getProperty("webui.preview.brand.font"); int brandFontPoint = configurationService.getIntProperty("webui.preview.brand.fontpoint"); JPEGFilter jpegFilter = new JPEGFilter(); - return jpegFilter - .getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, - brandFont); + return jpegFilter.getThumb( + currentItem, source, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, brandFont + ); } } diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java index 502f71eb5c..181e3bcc4b 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java @@ -8,22 +8,36 @@ package org.dspace.app.mediafilter; import java.awt.Color; +import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Transparency; +import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import javax.imageio.ImageIO; +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.Metadata; +import com.drew.metadata.MetadataException; +import com.drew.metadata.exif.ExifIFD0Directory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; +import org.w3c.dom.Node; /** * Filter image bitstreams, scaling the image to be within the bounds of @@ -33,6 +47,8 @@ import org.dspace.services.factory.DSpaceServicesFactory; * @author Jason Sherman jsherman@usao.edu */ public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats { + private static final Logger log = LogManager.getLogger(JPEGFilter.class); + @Override public String getFilteredName(String oldFilename) { return oldFilename + ".jpg"; @@ -62,6 +78,134 @@ public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats return "Generated Thumbnail"; } + /** + * Gets the rotation angle from image's metadata using ImageReader. + * This method consumes the InputStream, so you need to be careful to don't reuse the same InputStream after + * computing the rotation angle. + * + * @param buf InputStream of the image file + * @return Rotation angle in degrees (0, 90, 180, or 270) + */ + public static int getImageRotationUsingImageReader(InputStream buf) { + try { + Metadata metadata = ImageMetadataReader.readMetadata(buf); + ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); + if (directory != null && directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) { + return convertRotationToDegrees(directory.getInt(ExifIFD0Directory.TAG_ORIENTATION)); + } + } catch (MetadataException | ImageProcessingException | IOException e) { + log.error("Error reading image metadata", e); + } + return 0; + } + + public static int convertRotationToDegrees(int valueNode) { + // Common orientation values: + // 1 = Normal (0°) + // 6 = Rotated 90° CW + // 3 = Rotated 180° + // 8 = Rotated 270° CW + switch (valueNode) { + case 6: + return 90; + case 3: + return 180; + case 8: + return 270; + default: + return 0; + } + } + + /** + * Helper method to find a node with given name in metadata tree + */ + private static Node findNode(Node node, String name) { + if (node.getNodeName().equalsIgnoreCase(name)) { + return node; + } + + Node child = node.getFirstChild(); + while (child != null) { + Node found = findNode(child, name); + if (found != null) { + return found; + } + child = child.getNextSibling(); + } + return null; + } + + /** + * Rotates an image by the specified angle + * + * @param image The original image + * @param angle The rotation angle in degrees + * @return Rotated image + */ + public static BufferedImage rotateImage(BufferedImage image, int angle) { + if (angle == 0) { + return image; + } + + double radians = Math.toRadians(angle); + double sin = Math.abs(Math.sin(radians)); + double cos = Math.abs(Math.cos(radians)); + + int newWidth = (int) Math.round(image.getWidth() * cos + image.getHeight() * sin); + int newHeight = (int) Math.round(image.getWidth() * sin + image.getHeight() * cos); + + BufferedImage rotated = new BufferedImage(newWidth, newHeight, image.getType()); + Graphics2D g2d = rotated.createGraphics(); + AffineTransform at = new AffineTransform(); + + at.translate(newWidth / 2, newHeight / 2); + at.rotate(radians); + at.translate(-image.getWidth() / 2, -image.getHeight() / 2); + + g2d.setTransform(at); + g2d.drawImage(image, 0, 0, null); + g2d.dispose(); + + return rotated; + } + + /** + * Calculates scaled dimension while maintaining aspect ratio + * + * @param imgSize Original image dimensions + * @param boundary Maximum allowed dimensions + * @return New dimensions that fit within boundary while preserving aspect ratio + */ + private Dimension getScaledDimension(Dimension imgSize, Dimension boundary) { + + int originalWidth = imgSize.width; + int originalHeight = imgSize.height; + int boundWidth = boundary.width; + int boundHeight = boundary.height; + int newWidth = originalWidth; + int newHeight = originalHeight; + + + // First check if we need to scale width + if (originalWidth > boundWidth) { + // Scale width to fit + newWidth = boundWidth; + // Scale height to maintain aspect ratio + newHeight = (newWidth * originalHeight) / originalWidth; + } + + // Then check if we need to scale even with the new height + if (newHeight > boundHeight) { + // Scale height to fit instead + newHeight = boundHeight; + newWidth = (newHeight * originalWidth) / originalHeight; + } + + return new Dimension(newWidth, newHeight); + } + + /** * @param currentItem item * @param source source input stream @@ -72,10 +216,59 @@ public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats @Override public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) throws Exception { - // read in bitstream's image - BufferedImage buf = ImageIO.read(source); + return getThumb(currentItem, source, verbose); + } - return getThumb(currentItem, buf, verbose); + public InputStream getThumb(Item currentItem, InputStream source, boolean verbose) + throws Exception { + // get config params + final ConfigurationService configurationService + = DSpaceServicesFactory.getInstance().getConfigurationService(); + int xmax = configurationService + .getIntProperty("thumbnail.maxwidth"); + int ymax = configurationService + .getIntProperty("thumbnail.maxheight"); + boolean blurring = (boolean) configurationService + .getBooleanProperty("thumbnail.blurring"); + boolean hqscaling = (boolean) configurationService + .getBooleanProperty("thumbnail.hqscaling"); + + return getThumb(currentItem, source, verbose, xmax, ymax, blurring, hqscaling, 0, 0, null); + } + + protected InputStream getThumb( + Item currentItem, + InputStream source, + boolean verbose, + int xmax, + int ymax, + boolean blurring, + boolean hqscaling, + int brandHeight, + int brandFontPoint, + String brandFont + ) throws Exception { + + File tempFile = File.createTempFile("temp", ".tmp"); + tempFile.deleteOnExit(); + + // Write to temp file + try (FileOutputStream fos = new FileOutputStream(tempFile)) { + byte[] buffer = new byte[4096]; + int len; + while ((len = source.read(buffer)) != -1) { + fos.write(buffer, 0, len); + } + } + + int rotation = getImageRotationUsingImageReader(new FileInputStream(tempFile)); + // read in bitstream's image + BufferedImage buf = ImageIO.read(new FileInputStream(tempFile)); + + return getThumbDim( + currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, rotation, + brandFont + ); } public InputStream getThumb(Item currentItem, BufferedImage buf, boolean verbose) @@ -83,25 +276,28 @@ public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats // get config params final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - float xmax = (float) configurationService + int xmax = configurationService .getIntProperty("thumbnail.maxwidth"); - float ymax = (float) configurationService + int ymax = configurationService .getIntProperty("thumbnail.maxheight"); boolean blurring = (boolean) configurationService .getBooleanProperty("thumbnail.blurring"); boolean hqscaling = (boolean) configurationService .getBooleanProperty("thumbnail.hqscaling"); - return getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, 0, 0, null); + return getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, 0, 0, 0, null); } - public InputStream getThumbDim(Item currentItem, BufferedImage buf, boolean verbose, float xmax, float ymax, + public InputStream getThumbDim(Item currentItem, BufferedImage buf, boolean verbose, int xmax, int ymax, boolean blurring, boolean hqscaling, int brandHeight, int brandFontPoint, - String brandFont) + int rotation, String brandFont) throws Exception { - // now get the image dimensions - float xsize = (float) buf.getWidth(null); - float ysize = (float) buf.getHeight(null); + + // Rotate the image if needed + BufferedImage correctedImage = rotateImage(buf, rotation); + + int xsize = correctedImage.getWidth(); + int ysize = correctedImage.getHeight(); // if verbose flag is set, print out dimensions // to STDOUT @@ -109,77 +305,53 @@ public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats System.out.println("original size: " + xsize + "," + ysize); } - // scale by x first if needed - if (xsize > xmax) { - // calculate scaling factor so that xsize * scale = new size (max) - float scale_factor = xmax / xsize; + // Calculate new dimensions while maintaining aspect ratio + Dimension newDimension = getScaledDimension( + new Dimension(xsize, ysize), + new Dimension(xmax, ymax) + ); - // if verbose flag is set, print out extracted text - // to STDOUT - if (verbose) { - System.out.println("x scale factor: " + scale_factor); - } - - // now reduce x size - // and y size - xsize = xsize * scale_factor; - ysize = ysize * scale_factor; - - // if verbose flag is set, print out extracted text - // to STDOUT - if (verbose) { - System.out.println("size after fitting to maximum width: " + xsize + "," + ysize); - } - } - - // scale by y if needed - if (ysize > ymax) { - float scale_factor = ymax / ysize; - - // now reduce x size - // and y size - xsize = xsize * scale_factor; - ysize = ysize * scale_factor; - } // if verbose flag is set, print details to STDOUT if (verbose) { - System.out.println("size after fitting to maximum height: " + xsize + ", " - + ysize); + System.out.println("size after fitting to maximum height: " + newDimension.width + ", " + + newDimension.height); } + xsize = newDimension.width; + ysize = newDimension.height; + // create an image buffer for the thumbnail with the new xsize, ysize - BufferedImage thumbnail = new BufferedImage((int) xsize, (int) ysize, - BufferedImage.TYPE_INT_RGB); + BufferedImage thumbnail = new BufferedImage(xsize, ysize, BufferedImage.TYPE_INT_RGB); // Use blurring if selected in config. // a little blur before scaling does wonders for keeping moire in check. if (blurring) { // send the buffered image off to get blurred. - buf = getBlurredInstance((BufferedImage) buf); + correctedImage = getBlurredInstance(correctedImage); } // Use high quality scaling method if selected in config. // this has a definite performance penalty. if (hqscaling) { // send the buffered image off to get an HQ downscale. - buf = getScaledInstance((BufferedImage) buf, (int) xsize, (int) ysize, - (Object) RenderingHints.VALUE_INTERPOLATION_BICUBIC, (boolean) true); + correctedImage = getScaledInstance(correctedImage, xsize, ysize, + RenderingHints.VALUE_INTERPOLATION_BICUBIC, true); } // now render the image into the thumbnail buffer Graphics2D g2d = thumbnail.createGraphics(); - g2d.drawImage(buf, 0, 0, (int) xsize, (int) ysize, null); + g2d.drawImage(correctedImage, 0, 0, xsize, ysize, null); if (brandHeight != 0) { ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - Brand brand = new Brand((int) xsize, brandHeight, new Font(brandFont, Font.PLAIN, brandFontPoint), 5); + Brand brand = new Brand(xsize, brandHeight, new Font(brandFont, Font.PLAIN, brandFontPoint), 5); BufferedImage brandImage = brand.create(configurationService.getProperty("webui.preview.brand"), configurationService.getProperty("webui.preview.brand.abbrev"), currentItem == null ? "" : "hdl:" + currentItem.getHandle()); - g2d.drawImage(brandImage, (int) 0, (int) ysize, (int) xsize, (int) 20, null); + g2d.drawImage(brandImage, 0, ysize, xsize, 20, null); } // now create an input stream for the thumbnail buffer and return it diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java index 3acb6900db..577f1dec4a 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java @@ -81,6 +81,7 @@ public class PDFBoxThumbnail extends MediaFilter { // Generate thumbnail derivative and return as IO stream. JPEGFilter jpegFilter = new JPEGFilter(); + return jpegFilter.getThumb(currentItem, buf, verbose); } }