[DURACOM-243] Adds rotation handling inside JPEGFilter

Conflicts:

(cherry picked from commit 08e330c1c0)
This commit is contained in:
Vincenzo Mecca
2025-03-26 18:06:08 +01:00
parent f0abeea477
commit d137f8bb60
3 changed files with 232 additions and 68 deletions

View File

@@ -7,9 +7,7 @@
*/ */
package org.dspace.app.mediafilter; package org.dspace.app.mediafilter;
import java.awt.image.BufferedImage;
import java.io.InputStream; import java.io.InputStream;
import javax.imageio.ImageIO;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
@@ -63,27 +61,20 @@ public class BrandedPreviewJPEGFilter extends MediaFilter {
@Override @Override
public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose)
throws Exception { throws Exception {
// read in bitstream's image
BufferedImage buf = ImageIO.read(source);
// get config params // get config params
ConfigurationService configurationService ConfigurationService configurationService
= DSpaceServicesFactory.getInstance().getConfigurationService(); = DSpaceServicesFactory.getInstance().getConfigurationService();
float xmax = (float) configurationService int xmax = configurationService.getIntProperty("webui.preview.maxwidth");
.getIntProperty("webui.preview.maxwidth"); int ymax = configurationService.getIntProperty("webui.preview.maxheight");
float ymax = (float) configurationService boolean blurring = configurationService.getBooleanProperty("webui.preview.blurring");
.getIntProperty("webui.preview.maxheight"); boolean hqscaling = configurationService.getBooleanProperty("webui.preview.hqscaling");
boolean blurring = (boolean) configurationService
.getBooleanProperty("webui.preview.blurring");
boolean hqscaling = (boolean) configurationService
.getBooleanProperty("webui.preview.hqscaling");
int brandHeight = configurationService.getIntProperty("webui.preview.brand.height"); int brandHeight = configurationService.getIntProperty("webui.preview.brand.height");
String brandFont = configurationService.getProperty("webui.preview.brand.font"); String brandFont = configurationService.getProperty("webui.preview.brand.font");
int brandFontPoint = configurationService.getIntProperty("webui.preview.brand.fontpoint"); int brandFontPoint = configurationService.getIntProperty("webui.preview.brand.fontpoint");
JPEGFilter jpegFilter = new JPEGFilter(); JPEGFilter jpegFilter = new JPEGFilter();
return jpegFilter return jpegFilter.getThumb(
.getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, currentItem, source, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, brandFont
brandFont); );
} }
} }

View File

@@ -8,22 +8,36 @@
package org.dspace.app.mediafilter; package org.dspace.app.mediafilter;
import java.awt.Color; import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font; import java.awt.Font;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.Transparency; import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp; import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp; import java.awt.image.ConvolveOp;
import java.awt.image.Kernel; import java.awt.image.Kernel;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; 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 java.io.InputStream;
import javax.imageio.ImageIO; 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.content.Item;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.services.factory.DSpaceServicesFactory;
import org.w3c.dom.Node;
/** /**
* Filter image bitstreams, scaling the image to be within the bounds of * 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 * @author Jason Sherman jsherman@usao.edu
*/ */
public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats { public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats {
private static final Logger log = LogManager.getLogger(JPEGFilter.class);
@Override @Override
public String getFilteredName(String oldFilename) { public String getFilteredName(String oldFilename) {
return oldFilename + ".jpg"; return oldFilename + ".jpg";
@@ -62,6 +78,134 @@ public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats
return "Generated Thumbnail"; 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 currentItem item
* @param source source input stream * @param source source input stream
@@ -72,10 +216,59 @@ public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats
@Override @Override
public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose)
throws Exception { throws Exception {
// read in bitstream's image return getThumb(currentItem, source, verbose);
BufferedImage buf = ImageIO.read(source); }
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) public InputStream getThumb(Item currentItem, BufferedImage buf, boolean verbose)
@@ -83,25 +276,28 @@ public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats
// get config params // get config params
final ConfigurationService configurationService final ConfigurationService configurationService
= DSpaceServicesFactory.getInstance().getConfigurationService(); = DSpaceServicesFactory.getInstance().getConfigurationService();
float xmax = (float) configurationService int xmax = configurationService
.getIntProperty("thumbnail.maxwidth"); .getIntProperty("thumbnail.maxwidth");
float ymax = (float) configurationService int ymax = configurationService
.getIntProperty("thumbnail.maxheight"); .getIntProperty("thumbnail.maxheight");
boolean blurring = (boolean) configurationService boolean blurring = (boolean) configurationService
.getBooleanProperty("thumbnail.blurring"); .getBooleanProperty("thumbnail.blurring");
boolean hqscaling = (boolean) configurationService boolean hqscaling = (boolean) configurationService
.getBooleanProperty("thumbnail.hqscaling"); .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, boolean blurring, boolean hqscaling, int brandHeight, int brandFontPoint,
String brandFont) int rotation, String brandFont)
throws Exception { throws Exception {
// now get the image dimensions
float xsize = (float) buf.getWidth(null); // Rotate the image if needed
float ysize = (float) buf.getHeight(null); BufferedImage correctedImage = rotateImage(buf, rotation);
int xsize = correctedImage.getWidth();
int ysize = correctedImage.getHeight();
// if verbose flag is set, print out dimensions // if verbose flag is set, print out dimensions
// to STDOUT // to STDOUT
@@ -109,77 +305,53 @@ public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats
System.out.println("original size: " + xsize + "," + ysize); System.out.println("original size: " + xsize + "," + ysize);
} }
// scale by x first if needed // Calculate new dimensions while maintaining aspect ratio
if (xsize > xmax) { Dimension newDimension = getScaledDimension(
// calculate scaling factor so that xsize * scale = new size (max) new Dimension(xsize, ysize),
float scale_factor = xmax / xsize; 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 flag is set, print details to STDOUT
if (verbose) { if (verbose) {
System.out.println("size after fitting to maximum height: " + xsize + ", " System.out.println("size after fitting to maximum height: " + newDimension.width + ", "
+ ysize); + newDimension.height);
} }
xsize = newDimension.width;
ysize = newDimension.height;
// create an image buffer for the thumbnail with the new xsize, ysize // create an image buffer for the thumbnail with the new xsize, ysize
BufferedImage thumbnail = new BufferedImage((int) xsize, (int) ysize, BufferedImage thumbnail = new BufferedImage(xsize, ysize, BufferedImage.TYPE_INT_RGB);
BufferedImage.TYPE_INT_RGB);
// Use blurring if selected in config. // Use blurring if selected in config.
// a little blur before scaling does wonders for keeping moire in check. // a little blur before scaling does wonders for keeping moire in check.
if (blurring) { if (blurring) {
// send the buffered image off to get blurred. // send the buffered image off to get blurred.
buf = getBlurredInstance((BufferedImage) buf); correctedImage = getBlurredInstance(correctedImage);
} }
// Use high quality scaling method if selected in config. // Use high quality scaling method if selected in config.
// this has a definite performance penalty. // this has a definite performance penalty.
if (hqscaling) { if (hqscaling) {
// send the buffered image off to get an HQ downscale. // send the buffered image off to get an HQ downscale.
buf = getScaledInstance((BufferedImage) buf, (int) xsize, (int) ysize, correctedImage = getScaledInstance(correctedImage, xsize, ysize,
(Object) RenderingHints.VALUE_INTERPOLATION_BICUBIC, (boolean) true); RenderingHints.VALUE_INTERPOLATION_BICUBIC, true);
} }
// now render the image into the thumbnail buffer // now render the image into the thumbnail buffer
Graphics2D g2d = thumbnail.createGraphics(); 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) { if (brandHeight != 0) {
ConfigurationService configurationService ConfigurationService configurationService
= DSpaceServicesFactory.getInstance().getConfigurationService(); = 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"), BufferedImage brandImage = brand.create(configurationService.getProperty("webui.preview.brand"),
configurationService.getProperty("webui.preview.brand.abbrev"), configurationService.getProperty("webui.preview.brand.abbrev"),
currentItem == null ? "" : "hdl:" + currentItem.getHandle()); 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 // now create an input stream for the thumbnail buffer and return it

View File

@@ -81,6 +81,7 @@ public class PDFBoxThumbnail extends MediaFilter {
// Generate thumbnail derivative and return as IO stream. // Generate thumbnail derivative and return as IO stream.
JPEGFilter jpegFilter = new JPEGFilter(); JPEGFilter jpegFilter = new JPEGFilter();
return jpegFilter.getThumb(currentItem, buf, verbose); return jpegFilter.getThumb(currentItem, buf, verbose);
} }
} }